WIP: BRIN multi-range indexes

Started by Tomas Vondraabout 8 years ago179 messages
#1Tomas Vondra
tomas.vondra@2ndquadrant.com
1 attachment(s)

Hi all,

A couple of days ago I've shared a WIP patch [1]/messages/by-id/5d78b774-7e9c-c94e-12cf-fef51cc89b1a@2ndquadrant.com implementing BRIN
indexes based on bloom filters. One inherent limitation of that approach
is that it can only support equality conditions - that's perfectly fine
in many cases (e.g. with UUIDs it's rare to use range queries, etc.).

[1]: /messages/by-id/5d78b774-7e9c-c94e-12cf-fef51cc89b1a@2ndquadrant.com
/messages/by-id/5d78b774-7e9c-c94e-12cf-fef51cc89b1a@2ndquadrant.com

But in other cases that restriction is pretty unacceptable, e.g. with
timestamps that are queried mostly using range conditions. A common
issue is that while the data is initially well correlated (giving us
nice narrow min/max ranges in the BRIN index), this degrades over time
(typically due to DELETE/UPDATE and then new rows routed to free space).
There are not many options to prevent this, and fixing it pretty much
requires CLUSTER on the table.

This patch addresses this by BRIN indexes with more complex "summary".
Instead of keeping just a single "minmax interval", we maintain a list
of "minmax intervals", which allows us to track "gaps" in the data.

To illustrate the improvement, consider this table:

create table a (val float8) with (fillfactor = 90);
insert into a select i::float from generate_series(1,10000000) s(i);
update a set val = 1 where random() < 0.01;
update a set val = 10000000 where random() < 0.01;

Which means the column 'val' is almost perfectly correlated with the
position in the table (which would be great for BRIN minmax indexes),
but then 1% of the values is set to 1 and 10.000.000. That means pretty
much every range will be [1,10000000], which makes this BRIN index
mostly useless, as illustrated by these explain plans:

create index on a using brin (val) with (pages_per_range = 16);

explain analyze select * from a where val = 100;
QUERY PLAN
--------------------------------------------------------------------
Bitmap Heap Scan on a (cost=54.01..10691.02 rows=8 width=8)
(actual time=5.901..785.520 rows=1 loops=1)
Recheck Cond: (val = '100'::double precision)
Rows Removed by Index Recheck: 9999999
Heap Blocks: lossy=49020
-> Bitmap Index Scan on a_val_idx
(cost=0.00..54.00 rows=3400 width=0)
(actual time=5.792..5.792 rows=490240 loops=1)
Index Cond: (val = '100'::double precision)
Planning time: 0.119 ms
Execution time: 785.583 ms
(8 rows)

explain analyze select * from a where val between 100 and 10000;
QUERY PLAN
------------------------------------------------------------------
Bitmap Heap Scan on a (cost=55.94..25132.00 rows=7728 width=8)
(actual time=5.939..858.125 rows=9695 loops=1)
Recheck Cond: ((val >= '100'::double precision) AND
(val <= '10000'::double precision))
Rows Removed by Index Recheck: 9990305
Heap Blocks: lossy=49020
-> Bitmap Index Scan on a_val_idx
(cost=0.00..54.01 rows=10200 width=0)
(actual time=5.831..5.831 rows=490240 loops=1)
Index Cond: ((val >= '100'::double precision) AND
(val <= '10000'::double precision))
Planning time: 0.139 ms
Execution time: 871.132 ms
(8 rows)

Obviously, the queries do scan the whole table and then eliminate most
of the rows in "Index Recheck". Decreasing pages_per_range does not
really make a measurable difference in this case - it eliminates maybe
10% of the rechecks, but most pages still have very wide minmax range.

With the patch, it looks about like this:

create index on a using brin (val float8_minmax_multi_ops)
with (pages_per_range = 16);

explain analyze select * from a where val = 100;
QUERY PLAN
-------------------------------------------------------------------
Bitmap Heap Scan on a (cost=830.01..11467.02 rows=8 width=8)
(actual time=7.772..8.533 rows=1 loops=1)
Recheck Cond: (val = '100'::double precision)
Rows Removed by Index Recheck: 3263
Heap Blocks: lossy=16
-> Bitmap Index Scan on a_val_idx
(cost=0.00..830.00 rows=3400 width=0)
(actual time=7.729..7.729 rows=160 loops=1)
Index Cond: (val = '100'::double precision)
Planning time: 0.124 ms
Execution time: 8.580 ms
(8 rows)

explain analyze select * from a where val between 100 and 10000;
QUERY PLAN
------------------------------------------------------------------
Bitmap Heap Scan on a (cost=831.94..25908.00 rows=7728 width=8)
(actual time=9.318..23.715 rows=9695 loops=1)
Recheck Cond: ((val >= '100'::double precision) AND
(val <= '10000'::double precision))
Rows Removed by Index Recheck: 3361
Heap Blocks: lossy=64
-> Bitmap Index Scan on a_val_idx
(cost=0.00..830.01 rows=10200 width=0)
(actual time=9.274..9.274 rows=640 loops=1)
Index Cond: ((val >= '100'::double precision) AND
(val <= '10000'::double precision))
Planning time: 0.138 ms
Execution time: 36.100 ms
(8 rows)

That is, the timings drop from 785ms/871ms to 9ms/36s. The index is a
bit larger (1700kB instead of 150kB), but it's still orders of
magnitudes smaller than btree index (which is ~214MB in this case).

The index build is slower than the regular BRIN indexes (about
comparable to btree), but I'm sure it can be significantly improved.
Also, I'm sure it's not bug-free.

Two additional notes:

1) The patch does break the current BRIN indexes, because it requires
passing all SearchKeys to the "consistent" BRIN function at once
(otherwise we couldn't eliminate individual intervals in the summary),
while currently the BRIN only deals with one SearchKey at a time. And I
haven't modified the existing brin_minmax_consistent() function (yeah,
I'm lazy, but this should be enough for interested people to try it out
I believe).

2) It only works with float8 (and also timestamp data types) for now,
but it should be straightforward to add support for additional data
types. Most of that will be about adding catalog definitions anyway.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

brin-multi-range-v1.patchtext/x-patch; name=brin-multi-range-v1.patchDownload
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 6ec3297..94d696e 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -29,9 +29,6 @@ typedef struct MinmaxMultiOpaque
 	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
 } MinmaxMultiOpaque;
 
-#define		MODE_VALUES			1
-#define		MODE_RANGES			2
-
 #define		MINMAX_MAX_VALUES	64
 
 typedef struct MinmaxMultiRanges
@@ -39,12 +36,13 @@ typedef struct MinmaxMultiRanges
 	/* varlena header (do not touch directly!) */
 	int32	vl_len_;
 
-	int		mode;		/* values or ranges in the data array */
+	/* maxvalues >= (2*nranges + nvalues) */
 	int		maxvalues;	/* maximum number of values in the buffer */
-	int		numvalues;	/* number of values in the data buffer */
+	int		nranges;	/* number of ranges stored in the array */
+	int		nvalues;	/* number of values in the data array */
 
 	/* values stored for this range - either raw values, or ranges */
-	Datum 	data[FLEXIBLE_ARRAY_MEMBER];
+	Datum 	values[FLEXIBLE_ARRAY_MEMBER];
 
 } MinmaxMultiRanges;
 
@@ -68,13 +66,11 @@ minmax_multi_init(int maxvalues)
 	/*
 	 * Allocate the range list with space for the max number of values.
 	 */
-	len = offsetof(MinmaxMultiRanges, data) + maxvalues * sizeof(Datum);
+	len = offsetof(MinmaxMultiRanges, values) + maxvalues * sizeof(Datum);
 
 	ranges = (MinmaxMultiRanges *) palloc0(len);
 
-	ranges->mode = MINMAX_MAX_VALUES;
 	ranges->maxvalues = maxvalues;
-	ranges->mode = MODE_VALUES;	/* start by accumulating values */
 
 	SET_VARSIZE(ranges, len);
 
@@ -87,6 +83,12 @@ typedef struct compare_context
 	Oid			colloid;
 } compare_context;
 
+typedef struct DatumRange {
+	Datum	minval;
+	Datum	maxval;
+	bool	collapsed;
+} DatumRange;
+
 static int
 compare_values(const void *a, const void *b, void *arg)
 {
@@ -109,27 +111,77 @@ compare_values(const void *a, const void *b, void *arg)
 	return 0;
 }
 
+static int
+compare_ranges(const void *a, const void *b, void *arg)
+{
+	DatumRange ra = *(DatumRange *)a;
+	DatumRange rb = *(DatumRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra.minval, rb.minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb.minval, ra.minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
 static void
 print_range(char * label, int numvalues, Datum *values)
 {
-	int i;
+	int idx;
 	StringInfoData str;
 
 	initStringInfo(&str);
 
-	for (i = 0; i < (numvalues/2); i++)
+	idx = 0;
+	while (idx < 2*ranges->nranges)
+	{
+		if (idx == 0)
+			appendStringInfoString(&str, "RANGES: [");
+		else
+			appendStringInfoString(&str, ", ");
+
+		appendStringInfo(&str, "%d => [%.9f, %.9f]", idx/2, DatumGetFloat8(values[idx]), DatumGetFloat8(values[idx+1]));
+
+		idx += 2;
+	}
+
+	if (ranges->nranges > 0)
+		appendStringInfoString(&str, "]");
+
+	if ((ranges->nranges > 0) && (ranges->nvalues > 0))
+		appendStringInfoString(&str, " ");
+
+	while (idx < 2*ranges->nranges + ranges->nvalues)
 	{
-		if (i > 0)
+		if (idx == 2*ranges->nranges)
+			appendStringInfoString(&str, "VALUES: [");
+		else
 			appendStringInfoString(&str, ", ");
 
-		appendStringInfo(&str, "\n\t%d => [%.9f, %.9f]", i, DatumGetFloat8(values[2*i]), DatumGetFloat8(values[2*i+1]));
+		appendStringInfo(&str, "%.9f", DatumGetFloat8(values[idx]));
+
+		idx++;
 	}
 
+	if (ranges->nvalues > 0)
+		appendStringInfoString(&str, "]");
+
 	elog(WARNING, "%s : %s", label, str.data);
 
 	resetStringInfo(&str);
 	pfree(str.data);
 }
+*/
 
 /*
  * minmax_multi_contains_value
@@ -143,33 +195,18 @@ minmax_multi_contains_value(BrinDesc *bdesc, Oid colloid,
 	int			i;
 	FmgrInfo   *cmpFn;
 
-	Datum a, b;
-
-	memcpy(&a, &ranges->data[0], sizeof(Datum));
-	memcpy(&b, &ranges->data[ranges->numvalues-1], sizeof(Datum));
-
 	/*
-	 * If we're still collecting exact values, let's see if we have an
-	 * exact match by using equality strategy.
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
 	 */
-	if (ranges->mode == MODE_VALUES)
+	if (ranges->nranges > 0)
 	{
-		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
-											 BTEqualStrategyNumber);
+		Datum	compar;
+		bool	match = true;
 
-		for (i = 0; i < ranges->numvalues; i++)
-		{
-			Datum compar
-				= FunctionCall2Coll(cmpFn, colloid, newval, ranges->data[i]);
-
-			/* found an exact match */
-			if (DatumGetBool(compar))
-				return true;
-		}
-	}
-	else /* MODE_RANGES */
-	{
-		Datum compar;
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
 
 		/*
 		 * Otherwise, need to compare the new value with boundaries of all
@@ -178,36 +215,36 @@ minmax_multi_contains_value(BrinDesc *bdesc, Oid colloid,
 		 */
 		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
 											 BTLessStrategyNumber);
-		compar = FunctionCall2Coll(cmpFn, colloid, newval, ranges->data[0]);
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
 
 		/* smaller than the smallest value in the range list */
 		if (DatumGetBool(compar))
-			return false;
+			match = false;
 
 		/*
 		 * And now compare it to the existing maximum (last value in the
-		 * data array).
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
 		 */
-		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
-											 BTGreaterStrategyNumber);
-		compar = FunctionCall2Coll(cmpFn, colloid, newval,
-								   ranges->data[ranges->numvalues-1]);
-		if (DatumGetBool(compar))
-			return false;
+		if (match)
+		{
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
 
 		/*
-		 * So it's in the general range, but is it actually covered by
-		 * any of the ranges? Each range uses two values.
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
 		 */
-		for (i = 0; i < (ranges->numvalues/2); i++)
+		for (i = 0; i < ranges->nranges && match; i++)
 		{
-			Datum	minvalue = 0;
-			Datum	maxvalue = 0;
-			Datum	compar;
-
 			/* copy the min/max values from the ranges */
-			minvalue = ranges->data[2*i];
-			maxvalue = ranges->data[2*i+1];
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
 
 			/*
 			 * Otherwise, need to compare the new value with boundaries of all
@@ -235,7 +272,22 @@ minmax_multi_contains_value(BrinDesc *bdesc, Oid colloid,
 		}
 	}
 
-	/* the value is not covered by the BRIN tuple */
+	/* so we're done with the ranges, now let's inspect the exact values */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
 	return false;
 }
 
@@ -250,110 +302,207 @@ minmax_multi_add_value(BrinDesc *bdesc, Oid colloid,
 					   MinmaxMultiRanges *ranges, Datum newval)
 {
 	int			i;
-	Datum		values[MINMAX_MAX_VALUES+2];
-	int			nvalues;
-	double		mindistance;
-	int			minidx;
 
 	/* context for sorting */
 	compare_context cxt;
 
+	Assert(ranges->maxvalues >= 2*ranges->nranges + ranges->nvalues);
+
 	/*
-	 * If we're still collecting exact values, let's see if there's a bit
-	 * more space for one additional one.
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * If we get duplicates, it doesn't matter as we'll deduplicate the
+	 * values later.
 	 */
-	if (ranges->mode == MODE_VALUES)
+	if (ranges->maxvalues > 2*ranges->nranges + ranges->nvalues)
 	{
-		/* yep, there's free space, so let's just squash it in */
-		if (ranges->numvalues < ranges->maxvalues)
-		{
-			ranges->data[ranges->numvalues++] = newval;
-			return;
-		}
+		ranges->values[2*ranges->nranges + ranges->nvalues] = newval;
+		ranges->nvalues++;
+		return;
+	}
 
+	/*
+	 * There's not enough space, so try deduplicating the values array,
+	 * including the new value.
+	 *
+	 * XXX maybe try deduplicating using memcmp first, instead of using
+	 * the (possibly) fairly complex/expensive comparator.
+	 *
+	 * XXX The if is somewhat unnecessary, because nvalues is always >= 0
+	 * so we do this always.
+	 */
+	if (ranges->nvalues >= 0)
+	{
+		FmgrInfo   *cmpFn;
+		int			nvalues = ranges->nvalues + 1;	/* space for newval */
+		Datum	   *values = palloc(sizeof(Datum) * nvalues);
+		int			idx;
+		DatumRange *ranges_tmp;
+		int			nranges;
+		int			count;
+
+		/* sort the values */
 		cxt.colloid = colloid;
 		cxt.cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 												 BTLessStrategyNumber);
 
+		/* copy the existing value and the new value too */
+		memcpy(values, &ranges->values[2*ranges->nranges], ranges->nvalues * sizeof(Datum));
+		values[ranges->nvalues] = newval;
+
+		/* the actual sort of all the values */
+		qsort_arg(values, nvalues, sizeof(Datum), compare_values, (void *) &cxt);
+
+		/* equality for duplicate detection */
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												 BTEqualStrategyNumber);
+
+		/* keep the first value */
+		idx = 1;
+		for (i = 1; i < nvalues; i++)
+		{
+			Datum compar;
+
+			/* is this a new value (different from the previous one)? */
+			compar = FunctionCall2Coll(cmpFn, colloid, values[i-1], values[i]);
+
+			/* not equal, we have to store it */
+			if (!DatumGetBool(compar))
+				values[idx++] = values[i];
+		}
+
 		/*
-		 * Nope, we have to switch to list of ranges. We simply sort the
-		 * values and set the mode to MODE_RANGES.
+		 * Have we managed to reduce the number of values? if yes, we can just
+		 * copy it back and we're done.
 		 */
-		qsort_arg(ranges->data, ranges->numvalues, sizeof(Datum),
-				  compare_values, (void *) &cxt);
+		if (idx < nvalues)
+		{
+			memcpy(&ranges->values[2*ranges->nranges], values, idx * sizeof(Datum));
+			ranges->nvalues = idx;
+			pfree(values);
+			return;
+		}
 
-		ranges->mode = MODE_RANGES;
-	}
+		Assert(idx == nvalues);
 
-	Assert(ranges->mode == MODE_RANGES);
-	Assert(ranges->maxvalues == ranges->numvalues);
+		/*
+		 * Nope, that didn't work, we have to merge some of the ranges. To do
+		 * that we'll turn the values to "collapsed" ranges (min==max), and
+		 * then merge a bunch of "closest ranges to cut the space requirements
+		 * in half.
+		 *
+		 * XXX Do a merge sort, instead of just using qsort.
+		 */
+		nranges = (ranges->nranges + nvalues);
+		ranges_tmp = palloc0(sizeof(DatumRange) * nranges);
 
-	// print_range("START", ranges->numvalues, ranges->data);
+		idx = 0;
 
-	/*
-	 * In the range mode we're guaranteed to have a full data array, because
-	 * that's the only situation we switch to MODE_RANGES. So we need to
-	 * decide which two ranges to merge, to reduce the number of ranges
-	 * to fit back into the BRIN tuple.
-	 *
-	 * We do that by treating the new value as a collapsed range, and then
-	 * merge the two ranges closest to each other.
-	 */
+		/* ranges */
+		for (i = 0; i < ranges->nranges; i++)
+		{
+			ranges_tmp[idx].minval = ranges->values[2*i];
+			ranges_tmp[idx].maxval = ranges->values[2*i+1];
+			ranges_tmp[idx].collapsed = false;
+			idx++;
+		}
 
-	nvalues = ranges->numvalues;
-	memcpy(values, ranges->data, ranges->numvalues * sizeof(Datum));
+		/* values as collapsed ranges */
+		for (i = 0; i < nvalues; i++)
+		{
+			ranges_tmp[idx].minval = values[i];
+			ranges_tmp[idx].maxval = values[i];
+			ranges_tmp[idx].collapsed = true;
+			idx++;
+		}
 
-	/* add the new value as a collapsed range (that's why we add it twice) */
-	memcpy(&values[nvalues++], &newval, sizeof(Datum));
-	memcpy(&values[nvalues++], &newval, sizeof(Datum));
+		Assert(idx == nranges);
 
-	/* sort the data again */
-	cxt.colloid = colloid;
-	cxt.cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
-											 BTLessStrategyNumber);
+		/* sort the ranges */
+		qsort_arg(ranges_tmp, nranges, sizeof(DatumRange), compare_ranges, (void *) &cxt);
 
-	qsort_arg(values, nvalues, sizeof(Datum), compare_values, (void *) &cxt);
+		/* Now combine as many ranges until the number of values to store
+		 * gets to half of MINMAX_MAX_VALUES. The collapsed ranges will be
+		 * stored as a single value.
+		 */
+		count = ranges->nranges * 2 + nvalues;
 
-	// print_range("MIDDLE", nvalues, values);
+		while (count > MINMAX_MAX_VALUES/2)
+		{
+			int		minidx = 0;
+			double	mindistance = DatumGetFloat8(ranges_tmp[1].minval) - DatumGetFloat8(ranges_tmp[0].maxval);
 
-	/*
-	 * now look for the two ranges closest to each other
-	 *
-	 * FIXME This simply uses DatumGetFloat8, which works for timestamps,
-	 * but we need to support custom fuctions too. 
-	 */
-	minidx = 0;
-	mindistance = DatumGetFloat8(values[2]) - DatumGetFloat8(values[1]);
+			/* pick the two closest ranges */
+			for (i = 1; i < (nranges-1); i++)
+			{
+				double	distance = DatumGetFloat8(ranges_tmp[i+1].minval) - DatumGetFloat8(ranges_tmp[i-1].maxval);
+				if (distance < mindistance)
+				{
+					mindistance = distance;
+					minidx = i;
+				}
+			}
 
-	/* extra range at the end, for the collapsed range we added */
-	for (i = 0; i < (ranges->maxvalues/2); i++)
-	{
-		double distance
-			= DatumGetFloat8(values[2*(i+1)]) - DatumGetFloat8(values[2*(i+1)-1]);
+			/*
+			 * Update the count of Datum values we need to store, depending
+			 * on what type of ranges we merged.
+			 *
+			 * 2 - when both ranges are 'regular'
+			 * 1 - when regular + collapsed
+			 * 0 - when both collapsed
+			 */
+			if (!ranges_tmp[minidx].collapsed && !ranges_tmp[minidx+1].collapsed)	/* both regular */
+				count -= 2;
+			else if (!ranges_tmp[minidx].collapsed || !ranges_tmp[minidx+1].collapsed) /* one regular */
+				count -= 1;
 
-		if (distance < mindistance)
-		{
-			mindistance = distance;
-			minidx = i;
-		}
-	}
+			/*
+			 * combine the two selected ranges, the new range is definiely
+			 * not collapsed
+			 */
+			ranges_tmp[minidx].maxval = ranges_tmp[minidx+1].maxval;
+			ranges_tmp[minidx].collapsed = false;
 
-	memset(ranges->data, 0, ranges->maxvalues * sizeof(Datum));
+			for (i = minidx+1; i < nranges-1; i++)
+				ranges_tmp[i] = ranges_tmp[i+1];
 
-	/*
-	 * Now we know which ranges to merge, which we do simply while copying
-	 * the data back into the range list.
-	 */
-	memcpy(ranges->data, values, (2*minidx + 1) * sizeof(Datum));
+			nranges--;
 
-	// print_range("END1", ranges->numvalues, ranges->data);
+			/*
+			 * we can never get zero values
+			 *
+			 * XXX Actually we should never get below (MINMAX_MAX_VALUES/2 - 1)
+			 * values or so.
+			 */
+			Assert(count > 0);
+		}
 
-	memcpy(ranges->data + (2*minidx + 1), &values[2*minidx+3], (ranges->maxvalues - 2*minidx-1) * sizeof(Datum));
+		/* first copy in the regular ranges */
+		ranges->nranges = 0;
+		for (i = 0; i < nranges; i++)
+		{
+			if (!ranges_tmp[i].collapsed)
+			{
+				ranges->values[2*ranges->nranges    ] = ranges_tmp[i].minval;
+				ranges->values[2*ranges->nranges + 1] = ranges_tmp[i].maxval;
+				ranges->nranges++;
+			}
+		}
 
-	// print_range("END2", ranges->numvalues, ranges->data);
+		/* now copy in the collapsed ones */
+		ranges->nvalues = 0;
+		for (i = 0; i < nranges; i++)
+		{
+			if (ranges_tmp[i].collapsed)
+			{
+				ranges->values[2*ranges->nranges + ranges->nvalues] = ranges_tmp[i].minval;
+				ranges->nvalues++;
+			}
+		}
 
-	Assert(minmax_multi_contains_value(bdesc, colloid, attno, attr->atttypid,
-			ranges, newval));
+		pfree(ranges_tmp);
+		pfree(values);
+	}
 }
 
 
@@ -439,7 +588,7 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	/* */
 	minmax_multi_add_value(bdesc, colloid, attno, attr, ranges, newval);
 
-	PG_RETURN_BOOL(updated);
+	PG_RETURN_BOOL(true);
 }
 
 /*
@@ -462,6 +611,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
 	MinmaxMultiRanges	*ranges;
 	int			keyno;
 	int			rangeno;
+	int			i;
 
 	/*
 	 * First check if there are any IS NULL scan keys, and if we're
@@ -512,10 +662,10 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
 	ranges = (MinmaxMultiRanges *) DatumGetPointer(column->bv_values[0]);
 
 	/* inspect the ranges, and for each one evaluate the scan keys */
-	for (rangeno = 0; rangeno < (ranges->maxvalues/2); rangeno++)
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
 	{
-		Datum	minval = ranges->data[2*rangeno];
-		Datum	maxval = ranges->data[2*rangeno+1];
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
 
 		/* assume the range is matching, and we'll try to prove otherwise */
 		bool	matching = true;
@@ -543,11 +693,39 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
 					break;
 
 				case BTEqualStrategyNumber:
-					/* FIXME this inspects the whole range again ... */
-					matches = minmax_multi_contains_value(bdesc, colloid, attno,
-														  subtype, ranges, value);
-					break;
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
 
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
 				case BTGreaterEqualStrategyNumber:
 				case BTGreaterStrategyNumber:
 					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
@@ -577,6 +755,60 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
 			PG_RETURN_DATUM(BoolGetDatum(true));
 	}
 
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+				
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, value, val);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
 	PG_RETURN_DATUM(BoolGetDatum(false));
 }
 
#2Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#1)
2 attachment(s)
Re: WIP: BRIN multi-range indexes

Apparently I've managed to botch the git format-patch thing :-( Attached
are both patches (the first one adding BRIN bloom indexes, the other one
adding the BRIN multi-range). Hopefully I got it right this time ;-)

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-brin-bloom-v1.patchtext/x-patch; name=0001-brin-bloom-v1.patchDownload
From c27f02d2839e08ebcee852448ed3838c8932d2ea Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Mon, 23 Oct 2017 22:48:58 +0200
Subject: [PATCH 1/2] brin bloom v1

---
 doc/src/sgml/brin.sgml                   | 236 ++++++++++
 src/backend/access/brin/Makefile         |   2 +-
 src/backend/access/brin/brin_bloom.c     | 727 +++++++++++++++++++++++++++++++
 src/include/catalog/pg_amop.h            |  59 +++
 src/include/catalog/pg_amproc.h          | 153 +++++++
 src/include/catalog/pg_opclass.h         |  25 ++
 src/include/catalog/pg_opfamily.h        |  20 +
 src/include/catalog/pg_proc.h            |  10 +
 src/test/regress/expected/opr_sanity.out |   3 +-
 9 files changed, 1233 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index b7483df..49d53e1 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -118,6 +118,13 @@
    </thead>
    <tbody>
     <row>
+     <entry><literal>abstime_bloom_ops</literal></entry>
+     <entry><type>abstime</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>abstime_minmax_ops</literal></entry>
      <entry><type>abstime</type></entry>
      <entry>
@@ -129,6 +136,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>int8_bloom_ops</literal></entry>
+     <entry><type>bigint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int8_minmax_ops</literal></entry>
      <entry><type>bigint</type></entry>
      <entry>
@@ -180,6 +194,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>bytea_bloom_ops</literal></entry>
+     <entry><type>bytea</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>bytea_minmax_ops</literal></entry>
      <entry><type>bytea</type></entry>
      <entry>
@@ -191,6 +212,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>bpchar_bloom_ops</literal></entry>
+     <entry><type>character</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>bpchar_minmax_ops</literal></entry>
      <entry><type>character</type></entry>
      <entry>
@@ -202,6 +230,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>char_bloom_ops</literal></entry>
+     <entry><type>"char"</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>char_minmax_ops</literal></entry>
      <entry><type>"char"</type></entry>
      <entry>
@@ -213,6 +248,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>date_bloom_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>date_minmax_ops</literal></entry>
      <entry><type>date</type></entry>
      <entry>
@@ -224,6 +266,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>float8_bloom_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>float8_minmax_ops</literal></entry>
      <entry><type>double precision</type></entry>
      <entry>
@@ -235,6 +284,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>inet_bloom_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>inet_minmax_ops</literal></entry>
      <entry><type>inet</type></entry>
      <entry>
@@ -258,6 +314,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>int4_bloom_ops</literal></entry>
+     <entry><type>integer</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int4_minmax_ops</literal></entry>
      <entry><type>integer</type></entry>
      <entry>
@@ -269,6 +332,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>interval_bloom_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>interval_minmax_ops</literal></entry>
      <entry><type>interval</type></entry>
      <entry>
@@ -280,6 +350,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>macaddr_bloom_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>macaddr_minmax_ops</literal></entry>
      <entry><type>macaddr</type></entry>
      <entry>
@@ -291,6 +368,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>macaddr8_minmax_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
      <entry>
@@ -302,6 +386,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>name_bloom_ops</literal></entry>
+     <entry><type>name</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>name_minmax_ops</literal></entry>
      <entry><type>name</type></entry>
      <entry>
@@ -313,6 +404,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>numeric_bloom_ops</literal></entry>
+     <entry><type>numeric</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>numeric_minmax_ops</literal></entry>
      <entry><type>numeric</type></entry>
      <entry>
@@ -324,6 +422,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><type>pg_lsn</type></entry>
      <entry>
@@ -335,6 +440,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>oid_bloom_ops</literal></entry>
+     <entry><type>oid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>oid_minmax_ops</literal></entry>
      <entry><type>oid</type></entry>
      <entry>
@@ -366,6 +478,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>float4_bloom_ops</literal></entry>
+     <entry><type>real</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>float4_minmax_ops</literal></entry>
      <entry><type>real</type></entry>
      <entry>
@@ -377,6 +496,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>reltime_bloom_ops</literal></entry>
+     <entry><type>reltime</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>reltime_minmax_ops</literal></entry>
      <entry><type>reltime</type></entry>
      <entry>
@@ -388,6 +514,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>int2_bloom_ops</literal></entry>
+     <entry><type>smallint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int2_minmax_ops</literal></entry>
      <entry><type>smallint</type></entry>
      <entry>
@@ -399,6 +532,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>text_bloom_ops</literal></entry>
+     <entry><type>text</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>text_minmax_ops</literal></entry>
      <entry><type>text</type></entry>
      <entry>
@@ -410,6 +550,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>tid_bloom_ops</literal></entry>
+     <entry><type>tid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>tid_minmax_ops</literal></entry>
      <entry><type>tid</type></entry>
      <entry>
@@ -421,6 +568,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timestamp_bloom_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timestamp_minmax_ops</literal></entry>
      <entry><type>timestamp without time zone</type></entry>
      <entry>
@@ -432,6 +586,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timestamptz_minmax_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
      <entry>
@@ -443,6 +604,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>time_bloom_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>time_minmax_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
      <entry>
@@ -454,6 +622,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timetz_bloom_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timetz_minmax_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
      <entry>
@@ -465,6 +640,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>uuid_bloom_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>uuid_minmax_ops</literal></entry>
      <entry><type>uuid</type></entry>
      <entry>
@@ -814,6 +996,60 @@ typedef struct BrinOpcInfo
  </para>
 
  <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table">.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
+ <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
     The minmax operator class requires a full set of operators to be
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 5aef925..a76d927 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = brin.o brin_pageops.o brin_revmap.o brin_tuple.o brin_xlog.o \
-       brin_minmax.o brin_inclusion.o brin_validate.o
+       brin_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000..c08f686
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,727 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		4	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+#define		BLOOM_PHASE_SORTED		1
+#define		BLOOM_PHASE_HASH		2
+
+/* how many hashes to accumulate before hashing */
+#define		BLOOM_MAX_UNSORTED		32
+#define		BLOOM_GROW_BYTES		32
+#define		BLOOM_NDISTINCT			1000	/* number of distinct values */
+#define		BLOOM_ERROR_RATE		0.05	/* 2% false positive rate */
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* global bloom filter parameters */
+	uint32	phase;		/* phase (initially SORTED, then HASH) */
+	uint32	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (optimal) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* fields used only in the EXACT phase */
+	uint32	nvalues;	/* number of hashes stored (sorted + extra) */
+	uint32	nsorted;	/* number of uint32 hashes in sorted part */
+
+	/* bitmap of the bloom filter */
+	char 	bitmap[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	/* https://en.wikipedia.org/wiki/Bloom_filter */
+	int m;	/* number of bits */
+	int k;	/* number of hash functions */
+
+	Assert((ndistinct > 0) && (ndistinct < 10000));	/* 10k is mostly arbitrary limit */
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	k = round(log(2.0) * m / ndistinct);
+
+	elog(DEBUG1, "create bloom filter m=%d k=%d", m, k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 */
+	len = Max(offsetof(BloomFilter, bitmap), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->phase = BLOOM_PHASE_SORTED;
+	filter->nhashes = k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * XXX Actually, we don't need to do repalloc - we just need to set the
+ * varlena header length!
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i,
+			nvalues;
+	uint32 *values;
+
+	/* never call compact on filters in HASH phase */
+	Assert(filter->phase == BLOOM_PHASE_SORTED);
+
+	/* no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) palloc(filter->nvalues * sizeof(uint32));
+
+	/* copy the data, then reset the bitmap */
+	memcpy(values, filter->bitmap, filter->nvalues * sizeof(uint32));
+	memset(filter->bitmap, 0, filter->nvalues * sizeof(uint32));
+
+	/* FIXME optimization: sort only the unsorted part, then merge */
+	pg_qsort(values, filter->nvalues, sizeof(uint32), cmp_uint32);
+
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if a new value, keep it */
+		if (values[i] != values[i-1])
+		{
+			values[nvalues] = values[i];
+			nvalues++;
+		}
+	}
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	memcpy(filter->bitmap, values, nvalues * sizeof(uint32));
+
+	pfree(values);
+}
+
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter);
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+
+	/* assume 'not updated' by default */
+	if (updated)
+		*updated = false;
+
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (filter->phase == BLOOM_PHASE_SORTED)
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter,bitmap) + (filter->nvalues+1) * sizeof(uint32);
+
+			if (len < need)
+			{
+				/*
+				 * We don't double the size here, as in the first place we care about
+				 * reducing storage requirements, and the doubling happens automatically
+				 * in memory contexts anyway.
+				 *
+				 * XXX Zero the newly allocated part. Maybe not really needed?
+				 */
+				filter = (BloomFilter *) repalloc(filter, len + BLOOM_GROW_BYTES);
+				memset((char *)filter + len, 0, BLOOM_GROW_BYTES);
+				SET_VARSIZE(filter, len + BLOOM_GROW_BYTES);
+			}
+
+			/* copy the data into the bitmap */
+			memcpy(&filter->bitmap[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the regular hashing phase */
+	Assert(filter->phase == BLOOM_PHASE_HASH);
+
+	/* we're in the ah */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/*
+		 * Our "hash functions" are effectively hashes of the original
+		 * hash, with different seeds.
+		 */
+		uint32 h = ((uint32)DatumGetInt64(hash_uint32_extended(value, i))) % filter->nbits;
+
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->bitmap[byte] & (0x01 << bit)))
+		{
+			filter->bitmap[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	elog(WARNING, "switching %p to hashing (sorted %d, total %d)", filter, filter->nsorted, filter->nvalues);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, bitmap) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->phase = BLOOM_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->bitmap;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (filter->phase == BLOOM_PHASE_SORTED)
+	{
+		int i;
+		uint32 *values = (uint32 *)filter->bitmap;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+			if (value == values[i])
+				return true;
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(filter->phase == BLOOM_PHASE_HASH);
+
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* compute the hashes with a seed, used for the bloom filter */
+		uint32 h = ((uint32)DatumGetInt64(hash_uint32_extended(value, i))) % filter->nbits;
+
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->bitmap[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+/*
+ * bloom_filter_count
+ * 		Estimate the number of distinct values stored in the bloom filter.
+ */
+static int
+bloom_filter_count(BloomFilter *filter)
+{
+	if (filter->phase == BLOOM_PHASE_SORTED)
+		return (filter->nvalues + filter->nsorted);
+	else
+		return ceil(- (filter->nbits * 1.0 / filter->nhashes * log(1 - filter->nbits_set * 1.0 / filter->nbits)));
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 * 
+	 * bloom indexes only store a the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	/*
+	 * If the new value is null, we record that we saw it if it's the first
+	 * one; otherwise, there's nothing to do.
+	 */
+	if (isnull)
+	{
+		if (column->bv_hasnulls)
+			PG_RETURN_BOOL(false);
+
+		column->bv_hasnulls = true;
+		PG_RETURN_BOOL(true);
+	}
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(BLOOM_NDISTINCT, BLOOM_ERROR_RATE);
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) DatumGetPointer(column->bv_values[0]);
+
+	elog(DEBUG1, "brin_bloom_add_value: filter=%p bits=%d hashes=%d values=%d set=%d estimate=%d",
+				  filter, filter->nbits, filter->nhashes, filter->nvalues, filter->nbits_set,
+				  bloom_filter_count(filter));
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	column->bv_values[0] = PointerGetDatum(bloom_add_value(filter, hashValue, &updated));
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+
+	Assert(key->sk_attno == column->bv_attno);
+
+	/* handle IS NULL/IS NOT NULL tests */
+	if (key->sk_flags & SK_ISNULL)
+	{
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			if (column->bv_allnulls || column->bv_hasnulls)
+				PG_RETURN_BOOL(true);
+			PG_RETURN_BOOL(false);
+		}
+
+		/*
+		 * For IS NOT NULL, we can only skip ranges that are known to have
+		 * only nulls.
+		 */
+		if (key->sk_flags & SK_SEARCHNOTNULL)
+			PG_RETURN_BOOL(!column->bv_allnulls);
+
+		/*
+		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+		 * operators are strict and return false.
+		 */
+		PG_RETURN_BOOL(false);
+	}
+
+	/* if the range is all empty, it cannot possibly be consistent */
+	if (column->bv_allnulls)
+		PG_RETURN_BOOL(false);
+
+	filter = (BloomFilter *) DatumGetPointer(column->bv_values[0]);
+
+	Assert(filter);
+
+	attno = key->sk_attno;
+	value = key->sk_argument;
+	switch (key->sk_strategy)
+	{
+		case BloomEqualStrategyNumber:
+
+			/*
+			 * In the equality case (WHERE col = someval), we want to return
+			 * the current page range if the minimum value in the range <=
+			 * scan key, and the maximum value >= scan key.
+			 */
+			finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+			hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+			matches = bloom_contains_value(filter, hashValue);
+
+			break;
+		default:
+			/* shouldn't happen */
+			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+			matches = 0;
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+
+	/* Adjust "hasnulls" */
+	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+		col_a->bv_hasnulls = true;
+
+	/* If there are no values in B, there's nothing left to do */
+	if (col_b->bv_allnulls)
+		PG_RETURN_VOID();
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
+	 * B into A, and we're done.  We cannot run the operators in this case,
+	 * because values in A might contain garbage.  Note we already established
+	 * that B contains values.
+	 */
+	if (col_a->bv_allnulls)
+	{
+		col_a->bv_allnulls = false;
+		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
+										attr->attbyval, attr->attlen);
+		PG_RETURN_VOID();
+	}
+
+	/* FIXME merge the two bloom filters */
+	elog(WARNING, "FIXME: merge bloom filters");
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
diff --git a/src/include/catalog/pg_amop.h b/src/include/catalog/pg_amop.h
index f850be4..ef5b692 100644
--- a/src/include/catalog/pg_amop.h
+++ b/src/include/catalog/pg_amop.h
@@ -894,18 +894,24 @@ DATA(insert (	4064	 17   17 2 s	  1958	  3580 0 ));
 DATA(insert (	4064	 17   17 3 s	  1955	  3580 0 ));
 DATA(insert (	4064	 17   17 4 s	  1960	  3580 0 ));
 DATA(insert (	4064	 17   17 5 s	  1959	  3580 0 ));
+/* bloom bytea */
+DATA(insert (	5021	 17   17 1 s	  1955	  3580 0 ));
 /* minmax "char" */
 DATA(insert (	4062	 18   18 1 s	   631	  3580 0 ));
 DATA(insert (	4062	 18   18 2 s	   632	  3580 0 ));
 DATA(insert (	4062	 18   18 3 s		92	  3580 0 ));
 DATA(insert (	4062	 18   18 4 s	   634	  3580 0 ));
 DATA(insert (	4062	 18   18 5 s	   633	  3580 0 ));
+/* bloom "char" */
+DATA(insert (	5022	 18   18 1 s		92	  3580 0 ));
 /* minmax name */
 DATA(insert (	4065	 19   19 1 s	   660	  3580 0 ));
 DATA(insert (	4065	 19   19 2 s	   661	  3580 0 ));
 DATA(insert (	4065	 19   19 3 s		93	  3580 0 ));
 DATA(insert (	4065	 19   19 4 s	   663	  3580 0 ));
 DATA(insert (	4065	 19   19 5 s	   662	  3580 0 ));
+/* bloom name */
+DATA(insert (	5023	 19   19 1 s		93	  3580 0 ));
 /* minmax integer */
 DATA(insert (	4054	 20   20 1 s	   412	  3580 0 ));
 DATA(insert (	4054	 20   20 2 s	   414	  3580 0 ));
@@ -952,6 +958,16 @@ DATA(insert (	4054	 23   20 2 s		80	  3580 0 ));
 DATA(insert (	4054	 23   20 3 s		15	  3580 0 ));
 DATA(insert (	4054	 23   20 4 s		82	  3580 0 ));
 DATA(insert (	4054	 23   20 5 s		76	  3580 0 ));
+/* bloom integer */
+DATA(insert (	5024	 20   20 1 s	   410	  3580 0 ));
+DATA(insert (	5024	 20   21 1 s	  1868	  3580 0 ));
+DATA(insert (	5024	 20   23 1 s	   416	  3580 0 ));
+DATA(insert (	5024	 21   21 1 s		94	  3580 0 ));
+DATA(insert (	5024	 21   20 1 s	  1862	  3580 0 ));
+DATA(insert (	5024	 21   23 1 s	   532	  3580 0 ));
+DATA(insert (	5024	 23   23 1 s		96	  3580 0 ));
+DATA(insert (	5024	 23   21 1 s	   533	  3580 0 ));
+DATA(insert (	5024	 23   20 1 s		15	  3580 0 ));
 
 /* minmax text */
 DATA(insert (	4056	 25   25 1 s	   664	  3580 0 ));
@@ -959,12 +975,16 @@ DATA(insert (	4056	 25   25 2 s	   665	  3580 0 ));
 DATA(insert (	4056	 25   25 3 s		98	  3580 0 ));
 DATA(insert (	4056	 25   25 4 s	   667	  3580 0 ));
 DATA(insert (	4056	 25   25 5 s	   666	  3580 0 ));
+/* bloom text */
+DATA(insert (	5027	 25   25 1 s		98	  3580 0 ));
 /* minmax oid */
 DATA(insert (	4068	 26   26 1 s	   609	  3580 0 ));
 DATA(insert (	4068	 26   26 2 s	   611	  3580 0 ));
 DATA(insert (	4068	 26   26 3 s	   607	  3580 0 ));
 DATA(insert (	4068	 26   26 4 s	   612	  3580 0 ));
 DATA(insert (	4068	 26   26 5 s	   610	  3580 0 ));
+/* bloom oid */
+DATA(insert (	5028	 26   26 1 s	   607	  3580 0 ));
 /* minmax tid */
 DATA(insert (	4069	 27   27 1 s	  2799	  3580 0 ));
 DATA(insert (	4069	 27   27 2 s	  2801	  3580 0 ));
@@ -992,6 +1012,11 @@ DATA(insert (	4070	701  701 2 s	   673	  3580 0 ));
 DATA(insert (	4070	701  701 3 s	   670	  3580 0 ));
 DATA(insert (	4070	701  701 4 s	   675	  3580 0 ));
 DATA(insert (	4070	701  701 5 s	   674	  3580 0 ));
+/* bloom float (float4, float8) */
+DATA(insert (	5030	700  700 1 s	   620	  3580 0 ));
+DATA(insert (	5030	700  701 1 s	  1120	  3580 0 ));
+DATA(insert (	5030	701  700 1 s	  1130	  3580 0 ));
+DATA(insert (	5030	701  701 1 s	   670	  3580 0 ));
 
 /* minmax abstime */
 DATA(insert (	4072	702  702 1 s	   562	  3580 0 ));
@@ -999,24 +1024,32 @@ DATA(insert (	4072	702  702 2 s	   564	  3580 0 ));
 DATA(insert (	4072	702  702 3 s	   560	  3580 0 ));
 DATA(insert (	4072	702  702 4 s	   565	  3580 0 ));
 DATA(insert (	4072	702  702 5 s	   563	  3580 0 ));
+/* bloom abstime */
+DATA(insert (	5031	702  702 1 s	   560	  3580 0 ));
 /* minmax reltime */
 DATA(insert (	4073	703  703 1 s	   568	  3580 0 ));
 DATA(insert (	4073	703  703 2 s	   570	  3580 0 ));
 DATA(insert (	4073	703  703 3 s	   566	  3580 0 ));
 DATA(insert (	4073	703  703 4 s	   571	  3580 0 ));
 DATA(insert (	4073	703  703 5 s	   569	  3580 0 ));
+/* bloom reltime */
+DATA(insert (	5032	703  703 1 s	   566	  3580 0 ));
 /* minmax macaddr */
 DATA(insert (	4074	829  829 1 s	  1222	  3580 0 ));
 DATA(insert (	4074	829  829 2 s	  1223	  3580 0 ));
 DATA(insert (	4074	829  829 3 s	  1220	  3580 0 ));
 DATA(insert (	4074	829  829 4 s	  1225	  3580 0 ));
 DATA(insert (	4074	829  829 5 s	  1224	  3580 0 ));
+/* bloom macaddr */
+DATA(insert (	5033	829  829 1 s	  1220	  3580 0 ));
 /* minmax macaddr8 */
 DATA(insert (	4109	774  774 1 s	  3364	  3580 0 ));
 DATA(insert (	4109	774  774 2 s	  3365	  3580 0 ));
 DATA(insert (	4109	774  774 3 s	  3362	  3580 0 ));
 DATA(insert (	4109	774  774 4 s	  3367	  3580 0 ));
 DATA(insert (	4109	774  774 5 s	  3366	  3580 0 ));
+/* bloom macaddr8 */
+DATA(insert (	5034	774  774 1 s	  3362	  3580 0 ));
 /* minmax inet */
 DATA(insert (	4075	869  869 1 s	  1203	  3580 0 ));
 DATA(insert (	4075	869  869 2 s	  1204	  3580 0 ));
@@ -1030,18 +1063,24 @@ DATA(insert (	4102	869  869 8 s	   932	  3580 0 ));
 DATA(insert (	4102	869  869 18 s	  1201	  3580 0 ));
 DATA(insert (	4102	869  869 24 s	   933	  3580 0 ));
 DATA(insert (	4102	869  869 26 s	   931	  3580 0 ));
+/* bloom inet */
+DATA(insert (	5035	869  869 1 s	  1201	  3580 0 ));
 /* minmax character */
 DATA(insert (	4076   1042 1042 1 s	  1058	  3580 0 ));
 DATA(insert (	4076   1042 1042 2 s	  1059	  3580 0 ));
 DATA(insert (	4076   1042 1042 3 s	  1054	  3580 0 ));
 DATA(insert (	4076   1042 1042 4 s	  1061	  3580 0 ));
 DATA(insert (	4076   1042 1042 5 s	  1060	  3580 0 ));
+/* bloom character */
+DATA(insert (	5036   1042 1042 1 s	  1054	  3580 0 ));
 /* minmax time without time zone */
 DATA(insert (	4077   1083 1083 1 s	  1110	  3580 0 ));
 DATA(insert (	4077   1083 1083 2 s	  1111	  3580 0 ));
 DATA(insert (	4077   1083 1083 3 s	  1108	  3580 0 ));
 DATA(insert (	4077   1083 1083 4 s	  1113	  3580 0 ));
 DATA(insert (	4077   1083 1083 5 s	  1112	  3580 0 ));
+/* bloom time without time zone */
+DATA(insert (	5037   1083 1083 1 s	  1108	  3580 0 ));
 /* minmax datetime (date, timestamp, timestamptz) */
 DATA(insert (	4059   1114 1114 1 s	  2062	  3580 0 ));
 DATA(insert (	4059   1114 1114 2 s	  2063	  3580 0 ));
@@ -1088,6 +1127,16 @@ DATA(insert (	4059   1184 1184 2 s	  1323	  3580 0 ));
 DATA(insert (	4059   1184 1184 3 s	  1320	  3580 0 ));
 DATA(insert (	4059   1184 1184 4 s	  1325	  3580 0 ));
 DATA(insert (	4059   1184 1184 5 s	  1324	  3580 0 ));
+/* bloom datetime (date, timestamp, timestamptz) */
+DATA(insert (	5038   1114 1114 1 s	  2060	  3580 0 ));
+DATA(insert (	5038   1114 1082 1 s	  2373	  3580 0 ));
+DATA(insert (	5038   1114 1184 1 s	  2536	  3580 0 ));
+DATA(insert (	5038   1082 1082 1 s	  1093	  3580 0 ));
+DATA(insert (	5038   1082 1114 1 s	  2347	  3580 0 ));
+DATA(insert (	5038   1082 1184 1 s	  2360	  3580 0 ));
+DATA(insert (	5038   1184 1082 1 s	  2386	  3580 0 ));
+DATA(insert (	5038   1184 1114 1 s	  2542	  3580 0 ));
+DATA(insert (	5038   1184 1184 1 s	  1320	  3580 0 ));
 
 /* minmax interval */
 DATA(insert (	4078   1186 1186 1 s	  1332	  3580 0 ));
@@ -1095,12 +1144,16 @@ DATA(insert (	4078   1186 1186 2 s	  1333	  3580 0 ));
 DATA(insert (	4078   1186 1186 3 s	  1330	  3580 0 ));
 DATA(insert (	4078   1186 1186 4 s	  1335	  3580 0 ));
 DATA(insert (	4078   1186 1186 5 s	  1334	  3580 0 ));
+/* bloom interval */
+DATA(insert (	5041   1186 1186 1 s	  1330	  3580 0 ));
 /* minmax time with time zone */
 DATA(insert (	4058   1266 1266 1 s	  1552	  3580 0 ));
 DATA(insert (	4058   1266 1266 2 s	  1553	  3580 0 ));
 DATA(insert (	4058   1266 1266 3 s	  1550	  3580 0 ));
 DATA(insert (	4058   1266 1266 4 s	  1555	  3580 0 ));
 DATA(insert (	4058   1266 1266 5 s	  1554	  3580 0 ));
+/* bloom time with time zone */
+DATA(insert (	5042   1266 1266 1 s	  1550	  3580 0 ));
 /* minmax bit */
 DATA(insert (	4079   1560 1560 1 s	  1786	  3580 0 ));
 DATA(insert (	4079   1560 1560 2 s	  1788	  3580 0 ));
@@ -1119,12 +1172,16 @@ DATA(insert (	4055   1700 1700 2 s	  1755	  3580 0 ));
 DATA(insert (	4055   1700 1700 3 s	  1752	  3580 0 ));
 DATA(insert (	4055   1700 1700 4 s	  1757	  3580 0 ));
 DATA(insert (	4055   1700 1700 5 s	  1756	  3580 0 ));
+/* bloom numeric */
+DATA(insert (	5045   1700 1700 1 s	  1752	  3580 0 ));
 /* minmax uuid */
 DATA(insert (	4081   2950 2950 1 s	  2974	  3580 0 ));
 DATA(insert (	4081   2950 2950 2 s	  2976	  3580 0 ));
 DATA(insert (	4081   2950 2950 3 s	  2972	  3580 0 ));
 DATA(insert (	4081   2950 2950 4 s	  2977	  3580 0 ));
 DATA(insert (	4081   2950 2950 5 s	  2975	  3580 0 ));
+/* bloom uuid */
+DATA(insert (	5046   2950 2950 1 s	  2972	  3580 0 ));
 /* inclusion range types */
 DATA(insert (	4103   3831 3831  1 s	  3893	  3580 0 ));
 DATA(insert (	4103   3831 3831  2 s	  3895	  3580 0 ));
@@ -1146,6 +1203,8 @@ DATA(insert (	4082   3220 3220 2 s	  3226	  3580 0 ));
 DATA(insert (	4082   3220 3220 3 s	  3222	  3580 0 ));
 DATA(insert (	4082   3220 3220 4 s	  3227	  3580 0 ));
 DATA(insert (	4082   3220 3220 5 s	  3225	  3580 0 ));
+/* bloom pg_lsn */
+DATA(insert (	5047   3220 3220 1 s	  3222	  3580 0 ));
 /* inclusion box */
 DATA(insert (	4104	603  603  1 s	   493	  3580 0 ));
 DATA(insert (	4104	603  603  2 s	   494	  3580 0 ));
diff --git a/src/include/catalog/pg_amproc.h b/src/include/catalog/pg_amproc.h
index 1c95846..cef4a7c 100644
--- a/src/include/catalog/pg_amproc.h
+++ b/src/include/catalog/pg_amproc.h
@@ -341,16 +341,34 @@ DATA(insert (	4064	17	  17  1  3383 ));
 DATA(insert (	4064	17	  17  2  3384 ));
 DATA(insert (	4064	17	  17  3  3385 ));
 DATA(insert (	4064	17	  17  4  3386 ));
+/* bloom bytea */
+DATA(insert (	5021	17	  17  1  5017 ));
+DATA(insert (	5021	17	  17  2  5018 ));
+DATA(insert (	5021	17	  17  3  5019 ));
+DATA(insert (	5021	17	  17  4  5020 ));
+DATA(insert (	5021	17	  17 11   456 ));
 /* minmax "char" */
 DATA(insert (	4062	18	  18  1  3383 ));
 DATA(insert (	4062	18	  18  2  3384 ));
 DATA(insert (	4062	18	  18  3  3385 ));
 DATA(insert (	4062	18	  18  4  3386 ));
+/* bloom "char" */
+DATA(insert (	5022	18	  18  1  5017 ));
+DATA(insert (	5022	18	  18  2  5018 ));
+DATA(insert (	5022	18	  18  3  5019 ));
+DATA(insert (	5022	18	  18  4  5020 ));
+DATA(insert (	5022	18	  18 11   454 ));
 /* minmax name */
 DATA(insert (	4065	19	  19  1  3383 ));
 DATA(insert (	4065	19	  19  2  3384 ));
 DATA(insert (	4065	19	  19  3  3385 ));
 DATA(insert (	4065	19	  19  4  3386 ));
+/* bloom name */
+DATA(insert (	5023	19	  19  1  5017 ));
+DATA(insert (	5023	19	  19  2  5018 ));
+DATA(insert (	5023	19	  19  3  5019 ));
+DATA(insert (	5023	19	  19  4  5020 ));
+DATA(insert (	5023	19	  19 11   455 ));
 /* minmax integer: int2, int4, int8 */
 DATA(insert (	4054	20	  20  1  3383 ));
 DATA(insert (	4054	20	  20  2  3384 ));
@@ -391,16 +409,47 @@ DATA(insert (	4054	23	  21  2  3384 ));
 DATA(insert (	4054	23	  21  3  3385 ));
 DATA(insert (	4054	23	  21  4  3386 ));
 
+/* bloom integer: int2, int4, int8 */
+DATA(insert (	5024	20	  20  1  5017 ));
+DATA(insert (	5024	20	  20  2  5018 ));
+DATA(insert (	5024	20	  20  3  5019 ));
+DATA(insert (	5024	20	  20  4  5020 ));
+DATA(insert (	5024	20	  20 11   949 ));
+
+DATA(insert (	5024	21	  21  1  5017 ));
+DATA(insert (	5024	21	  21  2  5018 ));
+DATA(insert (	5024	21	  21  3  5019 ));
+DATA(insert (	5024	21	  21  4  5020 ));
+DATA(insert (	5024	21	  21 11   449 ));
+
+DATA(insert (	5024	23	  23  1  5017 ));
+DATA(insert (	5024	23	  23  2  5018 ));
+DATA(insert (	5024	23	  23  3  5019 ));
+DATA(insert (	5024	23	  23  4  5020 ));
+DATA(insert (	5024	23	  23 11   450 ));
+
 /* minmax text */
 DATA(insert (	4056	25	  25  1  3383 ));
 DATA(insert (	4056	25	  25  2  3384 ));
 DATA(insert (	4056	25	  25  3  3385 ));
 DATA(insert (	4056	25	  25  4  3386 ));
+/* bloom text */
+DATA(insert (	5027	25	  25  1  5017 ));
+DATA(insert (	5027	25	  25  2  5018 ));
+DATA(insert (	5027	25	  25  3  5019 ));
+DATA(insert (	5027	25	  25  4  5020 ));
+DATA(insert (	5027	25	  25 11   400 ));
 /* minmax oid */
 DATA(insert (	4068	26	  26  1  3383 ));
 DATA(insert (	4068	26	  26  2  3384 ));
 DATA(insert (	4068	26	  26  3  3385 ));
 DATA(insert (	4068	26	  26  4  3386 ));
+/* bloom oid */
+DATA(insert (	5028	26	  26  1  5017 ));
+DATA(insert (	5028	26	  26  2  5018 ));
+DATA(insert (	5028	26	  26  3  5019 ));
+DATA(insert (	5028	26	  26  4  5020 ));
+DATA(insert (	5028	26	  26 11   453 ));
 /* minmax tid */
 DATA(insert (	4069	27	  27  1  3383 ));
 DATA(insert (	4069	27	  27  2  3384 ));
@@ -427,26 +476,63 @@ DATA(insert (	4070   701	 700  2  3384 ));
 DATA(insert (	4070   701	 700  3  3385 ));
 DATA(insert (	4070   701	 700  4  3386 ));
 
+/* bloom float */
+DATA(insert (	5030   700	 700  1  5017 ));
+DATA(insert (	5030   700	 700  2  5018 ));
+DATA(insert (	5030   700	 700  3  5019 ));
+DATA(insert (	5030   700	 700  4  5020 ));
+DATA(insert (	5030   700	 700 11   451 ));
+
+DATA(insert (	5030   701	 701  1  5017 ));
+DATA(insert (	5030   701	 701  2  5018 ));
+DATA(insert (	5030   701	 701  3  5019 ));
+DATA(insert (	5030   701	 701  4  5020 ));
+DATA(insert (	5030   701	 701 11   452 ));
+
 /* minmax abstime */
 DATA(insert (	4072   702	 702  1  3383 ));
 DATA(insert (	4072   702	 702  2  3384 ));
 DATA(insert (	4072   702	 702  3  3385 ));
 DATA(insert (	4072   702	 702  4  3386 ));
+/* bloom abstime */
+DATA(insert (	5031   702	 702  1  5017 ));
+DATA(insert (	5031   702	 702  2  5018 ));
+DATA(insert (	5031   702	 702  3  5019 ));
+DATA(insert (	5031   702	 702  4  5020 ));
+DATA(insert (	5031   702	 702 11   450 ));
 /* minmax reltime */
 DATA(insert (	4073   703	 703  1  3383 ));
 DATA(insert (	4073   703	 703  2  3384 ));
 DATA(insert (	4073   703	 703  3  3385 ));
 DATA(insert (	4073   703	 703  4  3386 ));
+/* bloom reltime */
+DATA(insert (	5032   703	 703  1  5017 ));
+DATA(insert (	5032   703	 703  2  5018 ));
+DATA(insert (	5032   703	 703  3  5019 ));
+DATA(insert (	5032   703	 703  4  5020 ));
+DATA(insert (	5032   703	 703 11   450 ));
 /* minmax macaddr */
 DATA(insert (	4074   829	 829  1  3383 ));
 DATA(insert (	4074   829	 829  2  3384 ));
 DATA(insert (	4074   829	 829  3  3385 ));
 DATA(insert (	4074   829	 829  4  3386 ));
+/* bloom macaddr */
+DATA(insert (	5033   829	 829  1  5017 ));
+DATA(insert (	5033   829	 829  2  5018 ));
+DATA(insert (	5033   829	 829  3  5019 ));
+DATA(insert (	5033   829	 829  4  5020 ));
+DATA(insert (	5033   829	 829 11   399 ));
 /* minmax macaddr8 */
 DATA(insert (	4109   774	 774  1  3383 ));
 DATA(insert (	4109   774	 774  2  3384 ));
 DATA(insert (	4109   774	 774  3  3385 ));
 DATA(insert (	4109   774	 774  4  3386 ));
+/* bloom macaddr8 */
+DATA(insert (	5034   774	 774  1  5017 ));
+DATA(insert (	5034   774	 774  2  5018 ));
+DATA(insert (	5034   774	 774  3  5019 ));
+DATA(insert (	5034   774	 774  4  5020 ));
+DATA(insert (	5034   774	 774 11   328 ));
 /* minmax inet */
 DATA(insert (	4075   869	 869  1  3383 ));
 DATA(insert (	4075   869	 869  2  3384 ));
@@ -460,16 +546,34 @@ DATA(insert (	4102   869	 869  4  4108 ));
 DATA(insert (	4102   869	 869 11  4063 ));
 DATA(insert (	4102   869	 869 12  4071 ));
 DATA(insert (	4102   869	 869 13   930 ));
+/* bloom inet */
+DATA(insert (	5035   869	 869  1  5017 ));
+DATA(insert (	5035   869	 869  2  5018 ));
+DATA(insert (	5035   869	 869  3  5019 ));
+DATA(insert (	5035   869	 869  4  5020 ));
+DATA(insert (	5035   869	 869 11   422 ));
 /* minmax character */
 DATA(insert (	4076  1042	1042  1  3383 ));
 DATA(insert (	4076  1042	1042  2  3384 ));
 DATA(insert (	4076  1042	1042  3  3385 ));
 DATA(insert (	4076  1042	1042  4  3386 ));
+/* bloom character */
+DATA(insert (	5036  1042	1042  1  5017 ));
+DATA(insert (	5036  1042	1042  2  5018 ));
+DATA(insert (	5036  1042	1042  3  5019 ));
+DATA(insert (	5036  1042	1042  4  5020 ));
+DATA(insert (	5036  1042	1042 11   454 ));
 /* minmax time without time zone */
 DATA(insert (	4077  1083	1083  1  3383 ));
 DATA(insert (	4077  1083	1083  2  3384 ));
 DATA(insert (	4077  1083	1083  3  3385 ));
 DATA(insert (	4077  1083	1083  4  3386 ));
+/* bloom time without time zone */
+DATA(insert (	5037  1083	1083  1  5017 ));
+DATA(insert (	5037  1083	1083  2  5018 ));
+DATA(insert (	5037  1083	1083  3  5019 ));
+DATA(insert (	5037  1083	1083  4  5020 ));
+DATA(insert (	5037  1083	1083 11  1688 ));
 /* minmax datetime (date, timestamp, timestamptz) */
 DATA(insert (	4059  1114	1114  1  3383 ));
 DATA(insert (	4059  1114	1114  2  3384 ));
@@ -510,16 +614,47 @@ DATA(insert (	4059  1082	1184  2  3384 ));
 DATA(insert (	4059  1082	1184  3  3385 ));
 DATA(insert (	4059  1082	1184  4  3386 ));
 
+/* bloom datetime (date, timestamp, timestamptz) */
+DATA(insert (	5038  1114	1114  1  5017 ));
+DATA(insert (	5038  1114	1114  2  5018 ));
+DATA(insert (	5038  1114	1114  3  5019 ));
+DATA(insert (	5038  1114	1114  4  5020 ));
+DATA(insert (	5038  1114	1114 11  2039 ));
+
+DATA(insert (	5038  1184	1184  1  5017 ));
+DATA(insert (	5038  1184	1184  2  5018 ));
+DATA(insert (	5038  1184	1184  3  5019 ));
+DATA(insert (	5038  1184	1184  4  5020 ));
+DATA(insert (	5038  1184	1184 11  2039 ));
+
+DATA(insert (	5038  1082	1082  1  5017 ));
+DATA(insert (	5038  1082	1082  2  5018 ));
+DATA(insert (	5038  1082	1082  3  5019 ));
+DATA(insert (	5038  1082	1082  4  5020 ));
+DATA(insert (	5038  1082	1082 11  450 ));
+
 /* minmax interval */
 DATA(insert (	4078  1186	1186  1  3383 ));
 DATA(insert (	4078  1186	1186  2  3384 ));
 DATA(insert (	4078  1186	1186  3  3385 ));
 DATA(insert (	4078  1186	1186  4  3386 ));
+/* bloom interval */
+DATA(insert (	5041  1186	1186  1  5017 ));
+DATA(insert (	5041  1186	1186  2  5018 ));
+DATA(insert (	5041  1186	1186  3  5019 ));
+DATA(insert (	5041  1186	1186  4  5020 ));
+DATA(insert (	5041  1186	1186 11  1697 ));
 /* minmax time with time zone */
 DATA(insert (	4058  1266	1266  1  3383 ));
 DATA(insert (	4058  1266	1266  2  3384 ));
 DATA(insert (	4058  1266	1266  3  3385 ));
 DATA(insert (	4058  1266	1266  4  3386 ));
+/* bloom time with time zone */
+DATA(insert (	5042  1266	1266  1  5017 ));
+DATA(insert (	5042  1266	1266  2  5018 ));
+DATA(insert (	5042  1266	1266  3  5019 ));
+DATA(insert (	5042  1266	1266  4  5020 ));
+DATA(insert (	5042  1266	1266 11  1696 ));
 /* minmax bit */
 DATA(insert (	4079  1560	1560  1  3383 ));
 DATA(insert (	4079  1560	1560  2  3384 ));
@@ -535,11 +670,23 @@ DATA(insert (	4055  1700	1700  1  3383 ));
 DATA(insert (	4055  1700	1700  2  3384 ));
 DATA(insert (	4055  1700	1700  3  3385 ));
 DATA(insert (	4055  1700	1700  4  3386 ));
+/* bloom numeric */
+DATA(insert (	5045  1700	1700  1  5017 ));
+DATA(insert (	5045  1700	1700  2  5018 ));
+DATA(insert (	5045  1700	1700  3  5019 ));
+DATA(insert (	5045  1700	1700  4  5020 ));
+DATA(insert (	5045  1700	1700 11   432 ));
 /* minmax uuid */
 DATA(insert (	4081  2950	2950  1  3383 ));
 DATA(insert (	4081  2950	2950  2  3384 ));
 DATA(insert (	4081  2950	2950  3  3385 ));
 DATA(insert (	4081  2950	2950  4  3386 ));
+/* bloom uuid */
+DATA(insert (	5046  2950	2950  1  5017 ));
+DATA(insert (	5046  2950	2950  2  5018 ));
+DATA(insert (	5046  2950	2950  3  5019 ));
+DATA(insert (	5046  2950	2950  4  5020 ));
+DATA(insert (	5046  2950	2950 11  2963 ));
 /* inclusion range types */
 DATA(insert (	4103  3831	3831  1  4105 ));
 DATA(insert (	4103  3831	3831  2  4106 ));
@@ -553,6 +700,12 @@ DATA(insert (	4082  3220	3220  1  3383 ));
 DATA(insert (	4082  3220	3220  2  3384 ));
 DATA(insert (	4082  3220	3220  3  3385 ));
 DATA(insert (	4082  3220	3220  4  3386 ));
+/* bloom pg_lsn */
+DATA(insert (	5047  3220	3220  1  5017 ));
+DATA(insert (	5047  3220	3220  2  5018 ));
+DATA(insert (	5047  3220	3220  3  5019 ));
+DATA(insert (	5047  3220	3220  4  5020 ));
+DATA(insert (	5047  3220	3220 11  3252 ));
 /* inclusion box */
 DATA(insert (	4104   603	 603  1  4105 ));
 DATA(insert (	4104   603	 603  2  4106 ));
diff --git a/src/include/catalog/pg_opclass.h b/src/include/catalog/pg_opclass.h
index 28dbc74..ce28469 100644
--- a/src/include/catalog/pg_opclass.h
+++ b/src/include/catalog/pg_opclass.h
@@ -213,36 +213,61 @@ DATA(insert (	2742	jsonb_path_ops		PGNSP PGUID 4037  3802 f 23 ));
 /* BRIN operator classes */
 /* no brin opclass for bool */
 DATA(insert (	3580	bytea_minmax_ops		PGNSP PGUID 4064	17 t 17 ));
+DATA(insert (	3580	bytea_bloom_ops			PGNSP PGUID 5021	17 f 17 ));
 DATA(insert (	3580	char_minmax_ops			PGNSP PGUID 4062	18 t 18 ));
+DATA(insert (	3580	char_bloom_ops			PGNSP PGUID 5022	18 f 18 ));
 DATA(insert (	3580	name_minmax_ops			PGNSP PGUID 4065	19 t 19 ));
+DATA(insert (	3580	name_bloom_ops			PGNSP PGUID 5023	19 f 19 ));
 DATA(insert (	3580	int8_minmax_ops			PGNSP PGUID 4054	20 t 20 ));
+DATA(insert (	3580	int8_bloom_ops			PGNSP PGUID 5024	20 f 20 ));
 DATA(insert (	3580	int2_minmax_ops			PGNSP PGUID 4054	21 t 21 ));
+DATA(insert (	3580	int2_bloom_ops			PGNSP PGUID 5024	21 f 21 ));
 DATA(insert (	3580	int4_minmax_ops			PGNSP PGUID 4054	23 t 23 ));
+DATA(insert (	3580	int4_bloom_ops			PGNSP PGUID 5024	23 f 23 ));
 DATA(insert (	3580	text_minmax_ops			PGNSP PGUID 4056	25 t 25 ));
+DATA(insert (	3580	text_bloom_ops			PGNSP PGUID 5027	25 f 25 ));
 DATA(insert (	3580	oid_minmax_ops			PGNSP PGUID 4068	26 t 26 ));
+DATA(insert (	3580	oid_bloom_ops			PGNSP PGUID 5028	26 f 26 ));
 DATA(insert (	3580	tid_minmax_ops			PGNSP PGUID 4069	27 t 27 ));
 DATA(insert (	3580	float4_minmax_ops		PGNSP PGUID 4070   700 t 700 ));
+DATA(insert (	3580	float4_bloom_ops		PGNSP PGUID 5030   700 f 700 ));
 DATA(insert (	3580	float8_minmax_ops		PGNSP PGUID 4070   701 t 701 ));
+DATA(insert (	3580	float8_bloom_ops		PGNSP PGUID 5030   701 f 701 ));
 DATA(insert (	3580	abstime_minmax_ops		PGNSP PGUID 4072   702 t 702 ));
+DATA(insert (	3580	abstime_bloom_ops		PGNSP PGUID 5031   702 f 702 ));
 DATA(insert (	3580	reltime_minmax_ops		PGNSP PGUID 4073   703 t 703 ));
+DATA(insert (	3580	reltime_bloom_ops		PGNSP PGUID 5032   703 f 703 ));
 DATA(insert (	3580	macaddr_minmax_ops		PGNSP PGUID 4074   829 t 829 ));
+DATA(insert (	3580	macaddr_bloom_ops		PGNSP PGUID 5033   829 f 829 ));
 DATA(insert (	3580	macaddr8_minmax_ops		PGNSP PGUID 4109   774 t 774 ));
+DATA(insert (	3580	macaddr8_bloom_ops		PGNSP PGUID 5034   774 f 774 ));
 DATA(insert (	3580	inet_minmax_ops			PGNSP PGUID 4075   869 f 869 ));
 DATA(insert (	3580	inet_inclusion_ops		PGNSP PGUID 4102   869 t 869 ));
+DATA(insert (	3580	inet_bloom_ops			PGNSP PGUID 5035   869 f 869 ));
 DATA(insert (	3580	bpchar_minmax_ops		PGNSP PGUID 4076  1042 t 1042 ));
+DATA(insert (	3580	bpchar_bloom_ops		PGNSP PGUID 5036  1042 f 1042 ));
 DATA(insert (	3580	time_minmax_ops			PGNSP PGUID 4077  1083 t 1083 ));
+DATA(insert (	3580	time_bloom_ops			PGNSP PGUID 5037  1083 f 1083 ));
 DATA(insert (	3580	date_minmax_ops			PGNSP PGUID 4059  1082 t 1082 ));
+DATA(insert (	3580	date_bloom_ops			PGNSP PGUID 5038  1082 f 1082 ));
 DATA(insert (	3580	timestamp_minmax_ops	PGNSP PGUID 4059  1114 t 1114 ));
+DATA(insert (	3580	timestamp_bloom_ops		PGNSP PGUID 5038  1114 f 1114 ));
 DATA(insert (	3580	timestamptz_minmax_ops	PGNSP PGUID 4059  1184 t 1184 ));
+DATA(insert (	3580	timestamptz_bloom_ops	PGNSP PGUID 5038  1184 f 1184 ));
 DATA(insert (	3580	interval_minmax_ops		PGNSP PGUID 4078  1186 t 1186 ));
+DATA(insert (	3580	interval_bloom_ops		PGNSP PGUID 5041  1186 f 1186 ));
 DATA(insert (	3580	timetz_minmax_ops		PGNSP PGUID 4058  1266 t 1266 ));
+DATA(insert (	3580	timetz_bloom_ops		PGNSP PGUID 5042  1266 f 1266 ));
 DATA(insert (	3580	bit_minmax_ops			PGNSP PGUID 4079  1560 t 1560 ));
 DATA(insert (	3580	varbit_minmax_ops		PGNSP PGUID 4080  1562 t 1562 ));
 DATA(insert (	3580	numeric_minmax_ops		PGNSP PGUID 4055  1700 t 1700 ));
+DATA(insert (	3580	numeric_bloom_ops		PGNSP PGUID 5045  1700 f 1700 ));
 /* no brin opclass for record, anyarray */
 DATA(insert (	3580	uuid_minmax_ops			PGNSP PGUID 4081  2950 t 2950 ));
+DATA(insert (	3580	uuid_bloom_ops			PGNSP PGUID 5046  2950 f 2950 ));
 DATA(insert (	3580	range_inclusion_ops		PGNSP PGUID 4103  3831 t 3831 ));
 DATA(insert (	3580	pg_lsn_minmax_ops		PGNSP PGUID 4082  3220 t 3220 ));
+DATA(insert (	3580	pg_lsn_bloom_ops		PGNSP PGUID 5047  3220 f 3220 ));
 /* no brin opclass for enum, tsvector, tsquery, jsonb */
 DATA(insert (	3580	box_inclusion_ops		PGNSP PGUID 4104   603 t 603 ));
 /* no brin opclass for the geometric types except box */
diff --git a/src/include/catalog/pg_opfamily.h b/src/include/catalog/pg_opfamily.h
index 0d0ba7c..bf9c578 100644
--- a/src/include/catalog/pg_opfamily.h
+++ b/src/include/catalog/pg_opfamily.h
@@ -160,30 +160,50 @@ DATA(insert OID = 4036 (	2742	jsonb_ops		PGNSP PGUID ));
 DATA(insert OID = 4037 (	2742	jsonb_path_ops	PGNSP PGUID ));
 
 DATA(insert OID = 4054 (	3580	integer_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5024 (	3580	integer_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4055 (	3580	numeric_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5045 (	3580	numeric_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4056 (	3580	text_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5027 (	3580	text_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4058 (	3580	timetz_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5042 (	3580	timetz_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4059 (	3580	datetime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5038 (	3580	datetime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4062 (	3580	char_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5022 (	3580	char_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4064 (	3580	bytea_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5021 (	3580	bytea_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4065 (	3580	name_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5023 (	3580	name_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4068 (	3580	oid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5028 (	3580	oid_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4069 (	3580	tid_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 4070 (	3580	float_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5030 (	3580	float_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4072 (	3580	abstime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5031 (	3580	abstime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4073 (	3580	reltime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5032 (	3580	reltime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4074 (	3580	macaddr_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5033 (	3580	macaddr_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4109 (	3580	macaddr8_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5034 (	3580	macaddr8_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4075 (	3580	network_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 4102 (	3580	network_inclusion_ops	PGNSP PGUID ));
+DATA(insert OID = 5035 (	3580	network_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4076 (	3580	bpchar_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5036 (	3580	bpchar_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4077 (	3580	time_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5037 (	3580	time_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4078 (	3580	interval_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5041 (	3580	interval_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4079 (	3580	bit_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 4080 (	3580	varbit_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 4081 (	3580	uuid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5046 (	3580	uuid_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4103 (	3580	range_inclusion_ops		PGNSP PGUID ));
 DATA(insert OID = 4082 (	3580	pg_lsn_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5047 (	3580	pg_lsn_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4104 (	3580	box_inclusion_ops		PGNSP PGUID ));
 DATA(insert OID = 5000 (	4000	box_ops		PGNSP PGUID ));
 
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 93c031a..5852496 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -4374,6 +4374,16 @@ DESCR("BRIN inclusion support");
 DATA(insert OID = 4108 ( brin_inclusion_union	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_inclusion_union _null_ _null_ _null_ ));
 DESCR("BRIN inclusion support");
 
+/* BRIN bloom */
+DATA(insert OID = 5017 ( brin_bloom_opcinfo		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 1 0 2281 "2281" _null_ _null_ _null_ _null_ _null_ brin_bloom_opcinfo _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+DATA(insert OID = 5018 ( brin_bloom_add_value	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 4 0 16 "2281 2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_bloom_add_value _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+DATA(insert OID = 5019 ( brin_bloom_consistent	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_bloom_consistent _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+DATA(insert OID = 5020 ( brin_bloom_union		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_bloom_union _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+
 /* userlock replacements */
 DATA(insert OID = 2880 (  pg_advisory_lock				PGNSP PGUID 12 1 0 0 0 f f f f t f v u 1 0 2278 "20" _null_ _null_ _null_ _null_ _null_ pg_advisory_lock_int8 _null_ _null_ _null_ ));
 DESCR("obtain exclusive advisory lock");
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 684f7f2..91b2ecc 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -1819,6 +1819,7 @@ ORDER BY 1, 2, 3;
        2742 |           11 | ?&
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -1880,7 +1881,7 @@ ORDER BY 1, 2, 3;
        4000 |           25 | <<=
        4000 |           26 | >>
        4000 |           27 | >>=
-(121 rows)
+(122 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
-- 
2.9.5

0002-brin-multi-range-v1.patchtext/x-patch; name=0002-brin-multi-range-v1.patchDownload
From 723d186a99a8eb6f5595713615d4ad85bc4167bc Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 24 Oct 2017 23:35:27 +0200
Subject: [PATCH 2/2] brin multi-range v1

---
 src/backend/access/brin/Makefile            |   3 +-
 src/backend/access/brin/brin.c              |  36 +-
 src/backend/access/brin/brin_minmax_multi.c | 888 ++++++++++++++++++++++++++++
 src/include/catalog/pg_amop.h               |  52 ++
 src/include/catalog/pg_amproc.h             |  47 ++
 src/include/catalog/pg_opclass.h            |   4 +
 src/include/catalog/pg_opfamily.h           |   2 +
 src/include/catalog/pg_proc.h               |  10 +
 8 files changed, 1039 insertions(+), 3 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c

diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index a76d927..c87c796 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -13,6 +13,7 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = brin.o brin_pageops.o brin_revmap.o brin_tuple.o brin_xlog.o \
-       brin_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o
+       brin_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o \
+       brin_minmax_multi.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index b3aa6d1..ab8cd05 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -449,6 +449,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			else
 			{
 				int			keyno;
+				bool	   *attnos;
+				
+				attnos = palloc0(sizeof(bool) * bdesc->bd_tupdesc->natts);
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -465,6 +468,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
 					Datum		add;
 
+					/* all scan keys for the attribute */
+					ScanKey	   *keys;
+					int			nkeys;
+					int			i;
+
 					/*
 					 * The collation of the scan key must match the collation
 					 * used in the index column (but only if the search is not
@@ -487,6 +495,27 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 									   CurrentMemoryContext);
 					}
 
+					/* we process all scankeys on the first lookup */
+					if (attnos[keyattno - 1])
+						continue;
+					else
+						attnos[keyattno - 1] = true;
+
+					/*
+					 * OK, collect all scan keys for this column.
+					 */
+					keys = (ScanKey *) palloc0(scan->numberOfKeys * sizeof(ScanKey));
+
+					nkeys = 0;
+					for (i = 0; i < scan->numberOfKeys; i++)
+					{
+						/* scan is for the *current* attribute, so keep it */
+						if (key->sk_attno == keyattno)
+							keys[nkeys++] = &scan->keyData[i];
+					}
+
+					Assert((nkeys > 0) && (nkeys <= scan->numberOfKeys));
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -497,15 +526,18 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					 * the range as a whole, so break out of the loop as soon
 					 * as a false return value is obtained.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
+					add = FunctionCall4Coll(&consistentFn[keyattno - 1],
 											key->sk_collation,
 											PointerGetDatum(bdesc),
 											PointerGetDatum(bval),
-											PointerGetDatum(key));
+											PointerGetDatum(keys),
+											Int32GetDatum(nkeys));
 					addrange = DatumGetBool(add);
 					if (!addrange)
 						break;
 				}
+
+				pfree(attnos);
 			}
 		}
 
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000..94d696e
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,888 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+
+typedef struct MinmaxMultiOpaque
+{
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+#define		MINMAX_MAX_VALUES	64
+
+typedef struct MinmaxMultiRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* maxvalues >= (2*nranges + nvalues) */
+	int		maxvalues;	/* maximum number of values in the buffer */
+	int		nranges;	/* number of ranges stored in the array */
+	int		nvalues;	/* number of values in the data array */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum 	values[FLEXIBLE_ARRAY_MEMBER];
+
+} MinmaxMultiRanges;
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
+							 Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the range list, allocate all the memory.
+ */
+static MinmaxMultiRanges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	MinmaxMultiRanges  *ranges;
+
+	Assert(maxvalues > 0);
+	Assert(maxvalues <= 1024);	/* arbitrary limit */
+
+	/*
+	 * Allocate the range list with space for the max number of values.
+	 */
+	len = offsetof(MinmaxMultiRanges, values) + maxvalues * sizeof(Datum);
+
+	ranges = (MinmaxMultiRanges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	SET_VARSIZE(ranges, len);
+
+	return ranges;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+typedef struct DatumRange {
+	Datum	minval;
+	Datum	maxval;
+	bool	collapsed;
+} DatumRange;
+
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum va = *(Datum *)a;
+	Datum vb = *(Datum *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, va, vb);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, vb, va);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+static int
+compare_ranges(const void *a, const void *b, void *arg)
+{
+	DatumRange ra = *(DatumRange *)a;
+	DatumRange rb = *(DatumRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra.minval, rb.minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb.minval, ra.minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+static void
+print_range(char * label, int numvalues, Datum *values)
+{
+	int idx;
+	StringInfoData str;
+
+	initStringInfo(&str);
+
+	idx = 0;
+	while (idx < 2*ranges->nranges)
+	{
+		if (idx == 0)
+			appendStringInfoString(&str, "RANGES: [");
+		else
+			appendStringInfoString(&str, ", ");
+
+		appendStringInfo(&str, "%d => [%.9f, %.9f]", idx/2, DatumGetFloat8(values[idx]), DatumGetFloat8(values[idx+1]));
+
+		idx += 2;
+	}
+
+	if (ranges->nranges > 0)
+		appendStringInfoString(&str, "]");
+
+	if ((ranges->nranges > 0) && (ranges->nvalues > 0))
+		appendStringInfoString(&str, " ");
+
+	while (idx < 2*ranges->nranges + ranges->nvalues)
+	{
+		if (idx == 2*ranges->nranges)
+			appendStringInfoString(&str, "VALUES: [");
+		else
+			appendStringInfoString(&str, ", ");
+
+		appendStringInfo(&str, "%.9f", DatumGetFloat8(values[idx]));
+
+		idx++;
+	}
+
+	if (ranges->nvalues > 0)
+		appendStringInfoString(&str, "]");
+
+	elog(WARNING, "%s : %s", label, str.data);
+
+	resetStringInfo(&str);
+	pfree(str.data);
+}
+*/
+
+/*
+ * minmax_multi_contains_value
+ * 		See if the new value is already contained in the range list.
+ */
+static bool
+minmax_multi_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Oid typid,
+							MinmaxMultiRanges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpFn;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTLessStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	/* so we're done with the ranges, now let's inspect the exact values */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+
+/*
+ * minmax_multi_add_value
+ * 		See if the new value is already contained in the range list.
+ */
+static void
+minmax_multi_add_value(BrinDesc *bdesc, Oid colloid,
+					   AttrNumber attno, Form_pg_attribute attr,
+					   MinmaxMultiRanges *ranges, Datum newval)
+{
+	int			i;
+
+	/* context for sorting */
+	compare_context cxt;
+
+	Assert(ranges->maxvalues >= 2*ranges->nranges + ranges->nvalues);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * If we get duplicates, it doesn't matter as we'll deduplicate the
+	 * values later.
+	 */
+	if (ranges->maxvalues > 2*ranges->nranges + ranges->nvalues)
+	{
+		ranges->values[2*ranges->nranges + ranges->nvalues] = newval;
+		ranges->nvalues++;
+		return;
+	}
+
+	/*
+	 * There's not enough space, so try deduplicating the values array,
+	 * including the new value.
+	 *
+	 * XXX maybe try deduplicating using memcmp first, instead of using
+	 * the (possibly) fairly complex/expensive comparator.
+	 *
+	 * XXX The if is somewhat unnecessary, because nvalues is always >= 0
+	 * so we do this always.
+	 */
+	if (ranges->nvalues >= 0)
+	{
+		FmgrInfo   *cmpFn;
+		int			nvalues = ranges->nvalues + 1;	/* space for newval */
+		Datum	   *values = palloc(sizeof(Datum) * nvalues);
+		int			idx;
+		DatumRange *ranges_tmp;
+		int			nranges;
+		int			count;
+
+		/* sort the values */
+		cxt.colloid = colloid;
+		cxt.cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												 BTLessStrategyNumber);
+
+		/* copy the existing value and the new value too */
+		memcpy(values, &ranges->values[2*ranges->nranges], ranges->nvalues * sizeof(Datum));
+		values[ranges->nvalues] = newval;
+
+		/* the actual sort of all the values */
+		qsort_arg(values, nvalues, sizeof(Datum), compare_values, (void *) &cxt);
+
+		/* equality for duplicate detection */
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												 BTEqualStrategyNumber);
+
+		/* keep the first value */
+		idx = 1;
+		for (i = 1; i < nvalues; i++)
+		{
+			Datum compar;
+
+			/* is this a new value (different from the previous one)? */
+			compar = FunctionCall2Coll(cmpFn, colloid, values[i-1], values[i]);
+
+			/* not equal, we have to store it */
+			if (!DatumGetBool(compar))
+				values[idx++] = values[i];
+		}
+
+		/*
+		 * Have we managed to reduce the number of values? if yes, we can just
+		 * copy it back and we're done.
+		 */
+		if (idx < nvalues)
+		{
+			memcpy(&ranges->values[2*ranges->nranges], values, idx * sizeof(Datum));
+			ranges->nvalues = idx;
+			pfree(values);
+			return;
+		}
+
+		Assert(idx == nvalues);
+
+		/*
+		 * Nope, that didn't work, we have to merge some of the ranges. To do
+		 * that we'll turn the values to "collapsed" ranges (min==max), and
+		 * then merge a bunch of "closest ranges to cut the space requirements
+		 * in half.
+		 *
+		 * XXX Do a merge sort, instead of just using qsort.
+		 */
+		nranges = (ranges->nranges + nvalues);
+		ranges_tmp = palloc0(sizeof(DatumRange) * nranges);
+
+		idx = 0;
+
+		/* ranges */
+		for (i = 0; i < ranges->nranges; i++)
+		{
+			ranges_tmp[idx].minval = ranges->values[2*i];
+			ranges_tmp[idx].maxval = ranges->values[2*i+1];
+			ranges_tmp[idx].collapsed = false;
+			idx++;
+		}
+
+		/* values as collapsed ranges */
+		for (i = 0; i < nvalues; i++)
+		{
+			ranges_tmp[idx].minval = values[i];
+			ranges_tmp[idx].maxval = values[i];
+			ranges_tmp[idx].collapsed = true;
+			idx++;
+		}
+
+		Assert(idx == nranges);
+
+		/* sort the ranges */
+		qsort_arg(ranges_tmp, nranges, sizeof(DatumRange), compare_ranges, (void *) &cxt);
+
+		/* Now combine as many ranges until the number of values to store
+		 * gets to half of MINMAX_MAX_VALUES. The collapsed ranges will be
+		 * stored as a single value.
+		 */
+		count = ranges->nranges * 2 + nvalues;
+
+		while (count > MINMAX_MAX_VALUES/2)
+		{
+			int		minidx = 0;
+			double	mindistance = DatumGetFloat8(ranges_tmp[1].minval) - DatumGetFloat8(ranges_tmp[0].maxval);
+
+			/* pick the two closest ranges */
+			for (i = 1; i < (nranges-1); i++)
+			{
+				double	distance = DatumGetFloat8(ranges_tmp[i+1].minval) - DatumGetFloat8(ranges_tmp[i-1].maxval);
+				if (distance < mindistance)
+				{
+					mindistance = distance;
+					minidx = i;
+				}
+			}
+
+			/*
+			 * Update the count of Datum values we need to store, depending
+			 * on what type of ranges we merged.
+			 *
+			 * 2 - when both ranges are 'regular'
+			 * 1 - when regular + collapsed
+			 * 0 - when both collapsed
+			 */
+			if (!ranges_tmp[minidx].collapsed && !ranges_tmp[minidx+1].collapsed)	/* both regular */
+				count -= 2;
+			else if (!ranges_tmp[minidx].collapsed || !ranges_tmp[minidx+1].collapsed) /* one regular */
+				count -= 1;
+
+			/*
+			 * combine the two selected ranges, the new range is definiely
+			 * not collapsed
+			 */
+			ranges_tmp[minidx].maxval = ranges_tmp[minidx+1].maxval;
+			ranges_tmp[minidx].collapsed = false;
+
+			for (i = minidx+1; i < nranges-1; i++)
+				ranges_tmp[i] = ranges_tmp[i+1];
+
+			nranges--;
+
+			/*
+			 * we can never get zero values
+			 *
+			 * XXX Actually we should never get below (MINMAX_MAX_VALUES/2 - 1)
+			 * values or so.
+			 */
+			Assert(count > 0);
+		}
+
+		/* first copy in the regular ranges */
+		ranges->nranges = 0;
+		for (i = 0; i < nranges; i++)
+		{
+			if (!ranges_tmp[i].collapsed)
+			{
+				ranges->values[2*ranges->nranges    ] = ranges_tmp[i].minval;
+				ranges->values[2*ranges->nranges + 1] = ranges_tmp[i].maxval;
+				ranges->nranges++;
+			}
+		}
+
+		/* now copy in the collapsed ones */
+		ranges->nvalues = 0;
+		for (i = 0; i < nranges; i++)
+		{
+			if (ranges_tmp[i].collapsed)
+			{
+				ranges->values[2*ranges->nranges + ranges->nvalues] = ranges_tmp[i].minval;
+				ranges->nvalues++;
+			}
+		}
+
+		pfree(ranges_tmp);
+		pfree(values);
+	}
+}
+
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	bool		updated = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	MinmaxMultiRanges *ranges;
+
+	/*
+	 * If the new value is null, we record that we saw it if it's the first
+	 * one; otherwise, there's nothing to do.
+	 */
+	if (isnull)
+	{
+		if (column->bv_hasnulls)
+			PG_RETURN_BOOL(false);
+
+		column->bv_hasnulls = true;
+		PG_RETURN_BOOL(true);
+	}
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		ranges = minmax_multi_init(MINMAX_MAX_VALUES);
+		column->bv_values[0] = PointerGetDatum(ranges);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		ranges = (MinmaxMultiRanges *) DatumGetPointer(column->bv_values[0]);
+
+	/*
+	 * If the new value is already covered by the existing values (or ranges)
+	 * in the BRIN tuple, we're done. We can't really exit when we just
+	 * created the ranges.
+	 */
+	if (minmax_multi_contains_value(bdesc, colloid, attno, attr->atttypid, ranges, newval))
+		PG_RETURN_BOOL(updated);
+
+	/* */
+	minmax_multi_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	MinmaxMultiRanges	*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	/* if the range is all empty, it cannot possibly be consistent */
+	if (column->bv_allnulls)
+		PG_RETURN_BOOL(false);
+
+	ranges = (MinmaxMultiRanges *) DatumGetPointer(column->bv_values[0]);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+				
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, value, val);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	/* FIXME */
+	elog(WARNING, "brin_minmax_multi_union not implemented");
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors inclusion_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
diff --git a/src/include/catalog/pg_amop.h b/src/include/catalog/pg_amop.h
index ef5b692..2052ba7 100644
--- a/src/include/catalog/pg_amop.h
+++ b/src/include/catalog/pg_amop.h
@@ -1007,6 +1007,12 @@ DATA(insert (	4070	701  700 2 s	  1134	  3580 0 ));
 DATA(insert (	4070	701  700 3 s	  1130	  3580 0 ));
 DATA(insert (	4070	701  700 4 s	  1135	  3580 0 ));
 DATA(insert (	4070	701  700 5 s	  1133	  3580 0 ));
+DATA(insert (	4005	701  701 1 s	   672	  3580 0 ));
+DATA(insert (	4005	701  701 2 s	   673	  3580 0 ));
+DATA(insert (	4005	701  701 3 s	   670	  3580 0 ));
+DATA(insert (	4005	701  701 4 s	   675	  3580 0 ));
+DATA(insert (	4005	701  701 5 s	   674	  3580 0 ));
+/* minmax multi (float8) */
 DATA(insert (	4070	701  701 1 s	   672	  3580 0 ));
 DATA(insert (	4070	701  701 2 s	   673	  3580 0 ));
 DATA(insert (	4070	701  701 3 s	   670	  3580 0 ));
@@ -1127,6 +1133,52 @@ DATA(insert (	4059   1184 1184 2 s	  1323	  3580 0 ));
 DATA(insert (	4059   1184 1184 3 s	  1320	  3580 0 ));
 DATA(insert (	4059   1184 1184 4 s	  1325	  3580 0 ));
 DATA(insert (	4059   1184 1184 5 s	  1324	  3580 0 ));
+/* minmax multi (timestamp, timestamptz) */
+DATA(insert (	4006   1114 1114 1 s	  2062	  3580 0 ));
+DATA(insert (	4006   1114 1114 2 s	  2063	  3580 0 ));
+DATA(insert (	4006   1114 1114 3 s	  2060	  3580 0 ));
+DATA(insert (	4006   1114 1114 4 s	  2065	  3580 0 ));
+DATA(insert (	4006   1114 1114 5 s	  2064	  3580 0 ));
+DATA(insert (	4006   1114 1082 1 s	  2371	  3580 0 ));
+DATA(insert (	4006   1114 1082 2 s	  2372	  3580 0 ));
+DATA(insert (	4006   1114 1082 3 s	  2373	  3580 0 ));
+DATA(insert (	4006   1114 1082 4 s	  2374	  3580 0 ));
+DATA(insert (	4006   1114 1082 5 s	  2375	  3580 0 ));
+DATA(insert (	4006   1114 1184 1 s	  2534	  3580 0 ));
+DATA(insert (	4006   1114 1184 2 s	  2535	  3580 0 ));
+DATA(insert (	4006   1114 1184 3 s	  2536	  3580 0 ));
+DATA(insert (	4006   1114 1184 4 s	  2537	  3580 0 ));
+DATA(insert (	4006   1114 1184 5 s	  2538	  3580 0 ));
+DATA(insert (	4006   1082 1082 1 s	  1095	  3580 0 ));
+DATA(insert (	4006   1082 1082 2 s	  1096	  3580 0 ));
+DATA(insert (	4006   1082 1082 3 s	  1093	  3580 0 ));
+DATA(insert (	4006   1082 1082 4 s	  1098	  3580 0 ));
+DATA(insert (	4006   1082 1082 5 s	  1097	  3580 0 ));
+DATA(insert (	4006   1082 1114 1 s	  2345	  3580 0 ));
+DATA(insert (	4006   1082 1114 2 s	  2346	  3580 0 ));
+DATA(insert (	4006   1082 1114 3 s	  2347	  3580 0 ));
+DATA(insert (	4006   1082 1114 4 s	  2348	  3580 0 ));
+DATA(insert (	4006   1082 1114 5 s	  2349	  3580 0 ));
+DATA(insert (	4006   1082 1184 1 s	  2358	  3580 0 ));
+DATA(insert (	4006   1082 1184 2 s	  2359	  3580 0 ));
+DATA(insert (	4006   1082 1184 3 s	  2360	  3580 0 ));
+DATA(insert (	4006   1082 1184 4 s	  2361	  3580 0 ));
+DATA(insert (	4006   1082 1184 5 s	  2362	  3580 0 ));
+DATA(insert (	4006   1184 1082 1 s	  2384	  3580 0 ));
+DATA(insert (	4006   1184 1082 2 s	  2385	  3580 0 ));
+DATA(insert (	4006   1184 1082 3 s	  2386	  3580 0 ));
+DATA(insert (	4006   1184 1082 4 s	  2387	  3580 0 ));
+DATA(insert (	4006   1184 1082 5 s	  2388	  3580 0 ));
+DATA(insert (	4006   1184 1114 1 s	  2540	  3580 0 ));
+DATA(insert (	4006   1184 1114 2 s	  2541	  3580 0 ));
+DATA(insert (	4006   1184 1114 3 s	  2542	  3580 0 ));
+DATA(insert (	4006   1184 1114 4 s	  2543	  3580 0 ));
+DATA(insert (	4006   1184 1114 5 s	  2544	  3580 0 ));
+DATA(insert (	4006   1184 1184 1 s	  1322	  3580 0 ));
+DATA(insert (	4006   1184 1184 2 s	  1323	  3580 0 ));
+DATA(insert (	4006   1184 1184 3 s	  1320	  3580 0 ));
+DATA(insert (	4006   1184 1184 4 s	  1325	  3580 0 ));
+DATA(insert (	4006   1184 1184 5 s	  1324	  3580 0 ));
 /* bloom datetime (date, timestamp, timestamptz) */
 DATA(insert (	5038   1114 1114 1 s	  2060	  3580 0 ));
 DATA(insert (	5038   1114 1082 1 s	  2373	  3580 0 ));
diff --git a/src/include/catalog/pg_amproc.h b/src/include/catalog/pg_amproc.h
index cef4a7c..437d1fb 100644
--- a/src/include/catalog/pg_amproc.h
+++ b/src/include/catalog/pg_amproc.h
@@ -476,6 +476,13 @@ DATA(insert (	4070   701	 700  2  3384 ));
 DATA(insert (	4070   701	 700  3  3385 ));
 DATA(insert (	4070   701	 700  4  3386 ));
 
+/* minmax multi float */
+
+DATA(insert (	4005   701	 701  1  4001 ));
+DATA(insert (	4005   701	 701  2  4002 ));
+DATA(insert (	4005   701	 701  3  4003 ));
+DATA(insert (	4005   701	 701  4  4004 ));
+
 /* bloom float */
 DATA(insert (	5030   700	 700  1  5017 ));
 DATA(insert (	5030   700	 700  2  5018 ));
@@ -614,6 +621,46 @@ DATA(insert (	4059  1082	1184  2  3384 ));
 DATA(insert (	4059  1082	1184  3  3385 ));
 DATA(insert (	4059  1082	1184  4  3386 ));
 
+/* minmax multi (timestamp, timestamptz) */
+DATA(insert (	4006  1114	1114  1  4001 ));
+DATA(insert (	4006  1114	1114  2  4002 ));
+DATA(insert (	4006  1114	1114  3  4003 ));
+DATA(insert (	4006  1114	1114  4  4004 ));
+DATA(insert (	4006  1114	1184  1  4001 ));
+DATA(insert (	4006  1114	1184  2  4002 ));
+DATA(insert (	4006  1114	1184  3  4003 ));
+DATA(insert (	4006  1114	1184  4  4004 ));
+DATA(insert (	4006  1114	1082  1  4001 ));
+DATA(insert (	4006  1114	1082  2  4002 ));
+DATA(insert (	4006  1114	1082  3  4003 ));
+DATA(insert (	4006  1114	1082  4  4004 ));
+
+DATA(insert (	4006  1184	1184  1  4001 ));
+DATA(insert (	4006  1184	1184  2  4002 ));
+DATA(insert (	4006  1184	1184  3  4003 ));
+DATA(insert (	4006  1184	1184  4  4004 ));
+DATA(insert (	4006  1184	1114  1  4001 ));
+DATA(insert (	4006  1184	1114  2  4002 ));
+DATA(insert (	4006  1184	1114  3  4003 ));
+DATA(insert (	4006  1184	1114  4  4004 ));
+DATA(insert (	4006  1184	1082  1  4001 ));
+DATA(insert (	4006  1184	1082  2  4002 ));
+DATA(insert (	4006  1184	1082  3  4003 ));
+DATA(insert (	4006  1184	1082  4  4004 ));
+
+DATA(insert (	4006  1082	1082  1  4001 ));
+DATA(insert (	4006  1082	1082  2  4002 ));
+DATA(insert (	4006  1082	1082  3  4003 ));
+DATA(insert (	4006  1082	1082  4  4004 ));
+DATA(insert (	4006  1082	1114  1  4001 ));
+DATA(insert (	4006  1082	1114  2  4002 ));
+DATA(insert (	4006  1082	1114  3  4003 ));
+DATA(insert (	4006  1082	1114  4  4004 ));
+DATA(insert (	4006  1082	1184  1  4001 ));
+DATA(insert (	4006  1082	1184  2  4002 ));
+DATA(insert (	4006  1082	1184  3  4003 ));
+DATA(insert (	4006  1082	1184  4  4004 ));
+
 /* bloom datetime (date, timestamp, timestamptz) */
 DATA(insert (	5038  1114	1114  1  5017 ));
 DATA(insert (	5038  1114	1114  2  5018 ));
diff --git a/src/include/catalog/pg_opclass.h b/src/include/catalog/pg_opclass.h
index ce28469..8f43b66 100644
--- a/src/include/catalog/pg_opclass.h
+++ b/src/include/catalog/pg_opclass.h
@@ -232,6 +232,7 @@ DATA(insert (	3580	tid_minmax_ops			PGNSP PGUID 4069	27 t 27 ));
 DATA(insert (	3580	float4_minmax_ops		PGNSP PGUID 4070   700 t 700 ));
 DATA(insert (	3580	float4_bloom_ops		PGNSP PGUID 5030   700 f 700 ));
 DATA(insert (	3580	float8_minmax_ops		PGNSP PGUID 4070   701 t 701 ));
+DATA(insert (	3580	float8_minmax_multi_ops	PGNSP PGUID 4005   701 f 701 ));
 DATA(insert (	3580	float8_bloom_ops		PGNSP PGUID 5030   701 f 701 ));
 DATA(insert (	3580	abstime_minmax_ops		PGNSP PGUID 4072   702 t 702 ));
 DATA(insert (	3580	abstime_bloom_ops		PGNSP PGUID 5031   702 f 702 ));
@@ -249,10 +250,13 @@ DATA(insert (	3580	bpchar_bloom_ops		PGNSP PGUID 5036  1042 f 1042 ));
 DATA(insert (	3580	time_minmax_ops			PGNSP PGUID 4077  1083 t 1083 ));
 DATA(insert (	3580	time_bloom_ops			PGNSP PGUID 5037  1083 f 1083 ));
 DATA(insert (	3580	date_minmax_ops			PGNSP PGUID 4059  1082 t 1082 ));
+DATA(insert (	3580	date_minmax_multi_ops	PGNSP PGUID 4006  1082 f 1082 ));
 DATA(insert (	3580	date_bloom_ops			PGNSP PGUID 5038  1082 f 1082 ));
 DATA(insert (	3580	timestamp_minmax_ops	PGNSP PGUID 4059  1114 t 1114 ));
+DATA(insert (	3580	timestamp_minmax_multi_ops	PGNSP PGUID 4006  1114 f 1114 ));
 DATA(insert (	3580	timestamp_bloom_ops		PGNSP PGUID 5038  1114 f 1114 ));
 DATA(insert (	3580	timestamptz_minmax_ops	PGNSP PGUID 4059  1184 t 1184 ));
+DATA(insert (	3580	timestamptz_minmax_multi_ops	PGNSP PGUID 4006  1184 f 1184 ));
 DATA(insert (	3580	timestamptz_bloom_ops	PGNSP PGUID 5038  1184 f 1184 ));
 DATA(insert (	3580	interval_minmax_ops		PGNSP PGUID 4078  1186 t 1186 ));
 DATA(insert (	3580	interval_bloom_ops		PGNSP PGUID 5041  1186 f 1186 ));
diff --git a/src/include/catalog/pg_opfamily.h b/src/include/catalog/pg_opfamily.h
index bf9c578..90adbbb 100644
--- a/src/include/catalog/pg_opfamily.h
+++ b/src/include/catalog/pg_opfamily.h
@@ -168,6 +168,7 @@ DATA(insert OID = 5027 (	3580	text_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4058 (	3580	timetz_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 5042 (	3580	timetz_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4059 (	3580	datetime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4006 (	3580	datetime_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5038 (	3580	datetime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4062 (	3580	char_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 5022 (	3580	char_bloom_ops			PGNSP PGUID ));
@@ -179,6 +180,7 @@ DATA(insert OID = 4068 (	3580	oid_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 5028 (	3580	oid_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4069 (	3580	tid_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 4070 (	3580	float_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4005 (	3580	float_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5030 (	3580	float_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4072 (	3580	abstime_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 5031 (	3580	abstime_bloom_ops		PGNSP PGUID ));
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 5852496..87e91de 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -4364,6 +4364,16 @@ DESCR("BRIN minmax support");
 DATA(insert OID = 3386 ( brin_minmax_union		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_union _null_ _null_ _null_ ));
 DESCR("BRIN minmax support");
 
+/* BRIN minmax multi */
+DATA(insert OID = 4001 ( brin_minmax_multi_opcinfo	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 1 0 2281 "2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_opcinfo _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+DATA(insert OID = 4002 ( brin_minmax_multi_add_value	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 4 0 16 "2281 2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_add_value _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+DATA(insert OID = 4003 ( brin_minmax_multi_consistent PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_consistent _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+DATA(insert OID = 4004 ( brin_minmax_multi_union		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_union _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+
 /* BRIN inclusion */
 DATA(insert OID = 4105 ( brin_inclusion_opcinfo PGNSP PGUID 12 1 0 0 0 f f f f t f i s 1 0 2281 "2281" _null_ _null_ _null_ _null_ _null_ brin_inclusion_opcinfo _null_ _null_ _null_ ));
 DESCR("BRIN inclusion support");
-- 
2.9.5

#3Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#2)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

attached is a patch series that includes both the BRIN multi-range
minmax indexes discussed in this thread, and the BRIN bloom indexes
initially posted in [1]/messages/by-id/5d78b774-7e9c-c94e-12cf-fef51cc89b1a@2ndquadrant.com.

It seems easier to just deal with a single patch series, although we may
end up adding just one of those proposed opclasses.

There are 4 parts:

0001 - Modifies bringetbitmap() to pass all scan keys to the consistent
function at once. This is actually needed by the multi-minmax indexes,
but not really required for the others.

I'm wondering if this is a safechange, considering it affects the BRIN
interface. I.e. custom BRIN opclasses (perhaps in extensions) will be
broken by this change. Maybe we should extend the BRIN API to support
two versions of the consistent function - one that processes scan keys
one by one, and the other one that processes all of them at once.

0002 - Adds BRIN bloom indexes, along with opclasses for all built-in
data types (or at least those that also have regular BRIN opclasses).

0003 - Adds BRIN multi-minmax indexes, but only with float8 opclasses
(which also includes timestamp etc.). That should be good enough for
now, but adding support for other data types will require adding some
sort of "distance" operator which is needed for merging ranges (to pick
the two "closest" ones). For float8 it's simply a subtraction.

0004 - Moves dealing with IS [NOT] NULL search keys from opclasses to
bringetbitmap(). The code was exactly the same in all opclasses, so
moving it to bringetbitmap() seems right. It also allows some nice
optimizations where we can skip the consistent() function entirely,
although maybe that's useless. Also, maybe the there are opclasses that
actually need to deal with the NULL values in consistent() function?

regards

[1]: /messages/by-id/5d78b774-7e9c-c94e-12cf-fef51cc89b1a@2ndquadrant.com
/messages/by-id/5d78b774-7e9c-c94e-12cf-fef51cc89b1a@2ndquadrant.com

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzDownload
0002-BRIN-bloom-indexes.patch.gzapplication/gzip; name=0002-BRIN-bloom-indexes.patch.gzDownload
0003-BRIN-multi-range-minmax-indexes.patch.gzapplication/gzip; name=0003-BRIN-multi-range-minmax-indexes.patch.gzDownload
0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzapplication/gzip; name=0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzDownload
#4Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#3)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

Apparently there was some minor breakage due to duplicate OIDs, so here
is the patch series updated to current master.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzDownload
0002-BRIN-bloom-indexes.patch.gzapplication/gzip; name=0002-BRIN-bloom-indexes.patch.gzDownload
0003-BRIN-multi-range-minmax-indexes.patch.gzapplication/gzip; name=0003-BRIN-multi-range-minmax-indexes.patch.gzDownload
0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzapplication/gzip; name=0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzDownload
#5Michael Paquier
michael.paquier@gmail.com
In reply to: Tomas Vondra (#4)
Re: WIP: BRIN multi-range indexes

On Sun, Nov 19, 2017 at 5:45 AM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Apparently there was some minor breakage due to duplicate OIDs, so here
is the patch series updated to current master.

Moved to CF 2018-01.
--
Michael

#6Mark Dilger
hornschnorter@gmail.com
In reply to: Tomas Vondra (#4)
1 attachment(s)
Re: WIP: BRIN multi-range indexes

On Nov 18, 2017, at 12:45 PM, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

Hi,

Apparently there was some minor breakage due to duplicate OIDs, so here
is the patch series updated to current master.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
<0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gz><0002-BRIN-bloom-indexes.patch.gz><0003-BRIN-multi-range-minmax-indexes.patch.gz><0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gz>

After applying these four patches to my copy of master, the regression
tests fail for F_SATISFIES_HASH_PARTITION 5028 as attached.

mark

Attachments:

regression.diffsapplication/octet-stream; name=regression.diffsDownload
*** /Users/mark/master/postgresql/src/test/regress/expected/oidjoins.out	2017-11-10 06:50:30.000000000 -0800
--- /Users/mark/master/postgresql/src/test/regress/results/oidjoins.out	2017-12-19 11:31:42.000000000 -0800
***************
*** 621,629 ****
  FROM	pg_catalog.pg_opclass fk
  WHERE	opcfamily != 0 AND
  	NOT EXISTS(SELECT 1 FROM pg_catalog.pg_opfamily pk WHERE pk.oid = fk.opcfamily);
!  ctid | opcfamily 
! ------+-----------
! (0 rows)
  
  SELECT	ctid, opcintype
  FROM	pg_catalog.pg_opclass fk
--- 621,630 ----
  FROM	pg_catalog.pg_opclass fk
  WHERE	opcfamily != 0 AND
  	NOT EXISTS(SELECT 1 FROM pg_catalog.pg_opfamily pk WHERE pk.oid = fk.opcfamily);
!   ctid  | opcfamily 
! --------+-----------
!  (1,57) |      5028
! (1 row)
  
  SELECT	ctid, opcintype
  FROM	pg_catalog.pg_opclass fk

======================================================================

*** /Users/mark/master/postgresql/src/test/regress/expected/opr_sanity.out	2017-12-19 11:28:04.000000000 -0800
--- /Users/mark/master/postgresql/src/test/regress/results/opr_sanity.out	2017-12-19 11:31:44.000000000 -0800
***************
*** 1700,1709 ****
  -- Ask access methods to validate opclasses
  -- (this replaces a lot of SQL-level checks that used to be done in this file)
  SELECT oid, opcname FROM pg_opclass WHERE NOT amvalidate(oid);
!  oid | opcname 
! -----+---------
! (0 rows)
! 
  -- **************** pg_am ****************
  -- Look for illegal values in pg_am fields
  SELECT p1.oid, p1.amname
--- 1700,1706 ----
  -- Ask access methods to validate opclasses
  -- (this replaces a lot of SQL-level checks that used to be done in this file)
  SELECT oid, opcname FROM pg_opclass WHERE NOT amvalidate(oid);
! ERROR:  cache lookup failed for operator family 5028
  -- **************** pg_am ****************
  -- Look for illegal values in pg_am fields
  SELECT p1.oid, p1.amname
***************
*** 1901,1909 ****
  WHERE NOT EXISTS(SELECT 1 FROM pg_amop AS p2
                   WHERE p2.amopfamily = p1.opcfamily
                     AND binary_coercible(p1.opcintype, p2.amoplefttype));
!  opcname | opcfamily 
! ---------+-----------
! (0 rows)
  
  -- Check that each operator listed in pg_amop has an associated opclass,
  -- that is one whose opcintype matches oprleft (possibly by coercion).
--- 1898,1907 ----
  WHERE NOT EXISTS(SELECT 1 FROM pg_amop AS p2
                   WHERE p2.amopfamily = p1.opcfamily
                     AND binary_coercible(p1.opcintype, p2.amoplefttype));
!     opcname    | opcfamily 
! ---------------+-----------
!  oid_bloom_ops |      5028
! (1 row)
  
  -- Check that each operator listed in pg_amop has an associated opclass,
  -- that is one whose opcintype matches oprleft (possibly by coercion).
***************
*** 1918,1924 ****
                     AND binary_coercible(p2.opcintype, p1.amoplefttype));
   amopfamily | amopstrategy | amopopr 
  ------------+--------------+---------
! (0 rows)
  
  -- Operators that are primary members of opclasses must be immutable (else
  -- it suggests that the index ordering isn't fixed).  Operators that are
--- 1916,1923 ----
                     AND binary_coercible(p2.opcintype, p1.amoplefttype));
   amopfamily | amopstrategy | amopopr 
  ------------+--------------+---------
!        5029 |            1 |     607
! (1 row)
  
  -- Operators that are primary members of opclasses must be immutable (else
  -- it suggests that the index ordering isn't fixed).  Operators that are

======================================================================

#7Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Mark Dilger (#6)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

On 12/19/2017 08:38 PM, Mark Dilger wrote:

On Nov 18, 2017, at 12:45 PM, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

Hi,

Apparently there was some minor breakage due to duplicate OIDs, so here
is the patch series updated to current master.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
<0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gz><0002-BRIN-bloom-indexes.patch.gz><0003-BRIN-multi-range-minmax-indexes.patch.gz><0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gz>

After applying these four patches to my copy of master, the regression
tests fail for F_SATISFIES_HASH_PARTITION 5028 as attached.

D'oh! There was an incorrect OID referenced in pg_opclass, which was
also used by the satisfies_hash_partition() function. Fixed patches
attached.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzDownload
0002-BRIN-bloom-indexes.patch.gzapplication/gzip; name=0002-BRIN-bloom-indexes.patch.gzDownload
0003-BRIN-multi-range-minmax-indexes.patch.gzapplication/gzip; name=0003-BRIN-multi-range-minmax-indexes.patch.gzDownload
0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzapplication/gzip; name=0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzDownload
#8Mark Dilger
hornschnorter@gmail.com
In reply to: Tomas Vondra (#7)
Re: WIP: BRIN multi-range indexes

On Dec 19, 2017, at 5:16 PM, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

On 12/19/2017 08:38 PM, Mark Dilger wrote:

On Nov 18, 2017, at 12:45 PM, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

Hi,

Apparently there was some minor breakage due to duplicate OIDs, so here
is the patch series updated to current master.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
<0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gz><0002-BRIN-bloom-indexes.patch.gz><0003-BRIN-multi-range-minmax-indexes.patch.gz><0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gz>

After applying these four patches to my copy of master, the regression
tests fail for F_SATISFIES_HASH_PARTITION 5028 as attached.

D'oh! There was an incorrect OID referenced in pg_opclass, which was
also used by the satisfies_hash_partition() function. Fixed patches
attached.

Thanks! These fix the regression test failures. On my mac, all tests are now
passing. I have not yet looked any further into the merits of these patches,
however.

mark

#9Mark Dilger
hornschnorter@gmail.com
In reply to: Tomas Vondra (#7)
Re: WIP: BRIN multi-range indexes

On Dec 19, 2017, at 5:16 PM, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

On 12/19/2017 08:38 PM, Mark Dilger wrote:

On Nov 18, 2017, at 12:45 PM, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

Hi,

Apparently there was some minor breakage due to duplicate OIDs, so here
is the patch series updated to current master.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
<0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gz><0002-BRIN-bloom-indexes.patch.gz><0003-BRIN-multi-range-minmax-indexes.patch.gz><0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gz>

After applying these four patches to my copy of master, the regression
tests fail for F_SATISFIES_HASH_PARTITION 5028 as attached.

D'oh! There was an incorrect OID referenced in pg_opclass, which was
also used by the satisfies_hash_partition() function. Fixed patches
attached.

Your use of type ScanKey in src/backend/access/brin/brin.c is a bit confusing. A
ScanKey is defined elsewhere as a pointer to ScanKeyData. When you define
an array of ScanKeys, you use pointer-to-pointer style:

+ ScanKey **keys,
+ **nullkeys;

But when you allocate space for the array, you don't treat it that way:

+   keys = palloc0(sizeof(ScanKey) * bdesc->bd_tupdesc->natts);
+   nullkeys = palloc0(sizeof(ScanKey) * bdesc->bd_tupdesc->natts);

But then again when you use nullkeys, you treat it as a two-dimensional array:

+ nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;

and likewise when you allocate space within keys:

+ keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);

Could you please clarify? I have been awake a bit too long; hopefully, I am
not merely missing the obvious.

mark

#10Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Mark Dilger (#9)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

On 12/20/2017 03:37 AM, Mark Dilger wrote:

On Dec 19, 2017, at 5:16 PM, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

On 12/19/2017 08:38 PM, Mark Dilger wrote:

On Nov 18, 2017, at 12:45 PM, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

Hi,

Apparently there was some minor breakage due to duplicate OIDs, so here
is the patch series updated to current master.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
<0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gz><0002-BRIN-bloom-indexes.patch.gz><0003-BRIN-multi-range-minmax-indexes.patch.gz><0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gz>

After applying these four patches to my copy of master, the regression
tests fail for F_SATISFIES_HASH_PARTITION 5028 as attached.

D'oh! There was an incorrect OID referenced in pg_opclass, which was
also used by the satisfies_hash_partition() function. Fixed patches
attached.

Your use of type ScanKey in src/backend/access/brin/brin.c is a bit confusing. A
ScanKey is defined elsewhere as a pointer to ScanKeyData. When you define
an array of ScanKeys, you use pointer-to-pointer style:

+ ScanKey **keys,
+ **nullkeys;

But when you allocate space for the array, you don't treat it that way:

+   keys = palloc0(sizeof(ScanKey) * bdesc->bd_tupdesc->natts);
+   nullkeys = palloc0(sizeof(ScanKey) * bdesc->bd_tupdesc->natts);

But then again when you use nullkeys, you treat it as a two-dimensional array:

+ nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;

and likewise when you allocate space within keys:

+ keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);

Could you please clarify? I have been awake a bit too long; hopefully, I am
not merely missing the obvious.

Yeah, that's wrong - it should be "sizeof(ScanKey *)" instead. It's
harmless, though, because ScanKey itself is a pointer, so the size is
the same.

Attached is an updated version of the patch series, significantly
reworking and improving the multi-minmax part (the rest of the patch is
mostly as it was before).

I've significantly refactored and cleaned up the multi-minmax part, and
I'm much happier with it - no doubt there's room for further improvement
but overall it's much better.

I've also added proper sgml docs for this part, and support for more
data types including variable-length ones (all integer types, numeric,
float-based types including timestamps, uuid, and a couple of others).

At the API level, I needed to add one extra support procedure that
measures distance between two values of the data type. This is needed so
because we only keep a limited number of intervals for each range, and
once in a while we need to decide which of them to "merge" (and we
simply merge the closest ones).

I've passed the indexes through significant testing and fixed a couple
of silly bugs / memory leaks. Let's see if there are more.

Performance-wise, the CREATE INDEX seems a bit slow - it's about an
order of magnitude slower than regular BRIN. Some of that is expected
(we simply need to do more stuff to maintain multiple ranges), but
perhaps there's room for additional improvements - that's something I'd
like to work on next.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzDownload
0002-BRIN-bloom-indexes.patch.gzapplication/gzip; name=0002-BRIN-bloom-indexes.patch.gzDownload
0003-BRIN-multi-range-minmax-indexes.patch.gzapplication/gzip; name=0003-BRIN-multi-range-minmax-indexes.patch.gzDownload
��DRZ0003-BRIN-multi-range-minmax-indexes.patch�\ys�Jr���%�L��d]�$�eY~����X~��q�@`Hb`�0�$zw�{���EP�emR����@������{�~��PL=o��K��t����:[{���w��3��`��u0y��JO\�H\�Xl����9����V�
�y.>����y�#�R��r;�~���CW�/���T>�J�C�k��/���|g��xO��[�q�2��Y��s������OG;����oE��?z3)B?
�;�G������8�b��A:�#�P�n�h-nUr
?e��J���,��W��X�������|�}�	D�\'�a�@)��3	����l(R?�:u�:q,#o��`���tw(^e���K�9�#|-"��X&SXW�(������x���N�����>��~~��~���<�����y��'�f�$���T��dk�����A9�	�",Fu; C:D��VZ�tM"�7�TL"�q��~4��*��������H�^���AV:t��o�� �pC�j�i�%M�Z��X��U��I�,
|y���V���f �d�KG^A�v[q�4PNz0�8dJBO1����X%)��x���
;\�a7�t&��G��~��{�'R��6:_�U+�yCg\N��e���`��HUv,�@��:���2����<��P&3$�����A[x�����`4I�h�W���_��z�+z0�L@����q]�5
]8�r���T�	���a�����vw����g��Eo���Gn�yr��j6�gWN�����{�w��8Qnm ��w����8\����o��	�`QF���gT�<j{G��7����X��	�����������p��*0hp�{;;��O���O�b0������4Y��M�U�����������'w���J�m���t_�����>�������:��'�m�)Q���.{|�/>8J'x�,+K��G#�8'Z��OC�Ft����$��~���v���{�������c����*�Fd�Bd�Qd�-�&�R�
iU��T�q0N��kP���*Cx�vh���>�>���kx�g�u����9�Y���d�{��G��a=p�����a6���?����H�|��W%�Z��
�7&��{���
+x��}x�"���5��G5�oU�`���������$�|����C�U�/���X�p=KT���|��i�������[(ON�N��M�+@o�b�<�*C��I�i���[>���p�=sr�u9�I|H�8�&������r��V��LM�D�������Iw9�
��B���z�G��a�i2�P8������{8����V�����>Q1�$�@��H�<	�� �CJ4'�D�CH�Z��OI�>3�9%� �2�}�dml���J�&������*K\��'�l
���f�H�|�	09��T6���0������S%Q92m�^���R��lR���f8JS��yH�+v31�^�H����U��a
C�S��U���
�O�n�����
-AzM��P-AzUEy�� ���|�� ���|�� ��}���'���A�|�N�`�b���v{����[��Q����KP�J+��=F���0d�3*��5X�x��ejjZ���m�X�A��Fs-��V��H�n���9Vl�Y�]	��~�L1i��\��s�z�	r�V�h�h����Y�2M�L`t��l���@�P�Ft�j|��|���HP��P�P7j%=�t���M����`,�2#����h���	o��a-U���sVH�����E����%�,��N�7� 10[1�!�.���>���IP��"D�N���y�]����c�?.����^sQ��jVH�R?�~o��)M�S���%��"p�b���){$0"�	`M\ol�4pw.o5�z�$�f�����+�*�RLud���i�J��o5OJ:��x�E.�#{�b�T�uB�F�{4��Z�����|9�wEF���=}���h�����c��Ep�1��Z�c97��V������X>��)�br��o�,��K��U��{�@j=� }�<+�yy��J�[�`����s���s>�4+T.�fF%���W��
���:swY�v��P�1`
�O��#�CJ������hb�X��dZ�3S<���@�����^�r������>�W�6�zM��b�;��=����p��������k�������n=E8?�	���
O�!�H��G�o�J\�[��V���$��@M����W�t������X����	��~J3�u��R��/L��O���w�������(�~��K�����N��#.���Jd���n��D���)M����=m:x��-��O��~���p8�y&�v�H�F��EY�����P���1hR�Ne_�l�F� ����x��:�Yw9@@"�(.HG��	t�w����{����T�����S�q�x�7�N�>�������I�H)^���?��ZCi�/>���m~�s���\�wNAQ���w,[D0_fZT�����j'HO�iRQLo����Sxd��Dlu����@���i�n�e:��03�<+'��j��]��K:E���*��sh`�����*=���Z7�o���}"������*(�1�d�BS����
y������D�5��Pq�=������f���8�a�[��M�f*%�w�����*z�b��c������k��9��$��2�t���+e���2���jIZ�r�������g��9�Gb:A� �?�����O��h�����+Q��d8���[���6�����j�):�
Q���H����E7.���jjx7����:�6�p���B��25(2V]"JN�"K�(���`�Z��l
�����x6H�M�h�V��]��|���@��]EboGt��������oW�����~>������5[�a�C�Ah�=hK����sG��	�4���2�a�)�
���Y8	��Ob���$���v��VN��*�K�VEg��
��j�aC��d7�e��	��o�o��3M@D:v �A�ba��++���C�!Y�-�-X���Ug!b����:X����i1��EY��D��\L�E`��'A�Pf�V%af4��D����?P�[�rnK�������d���&X����+��Ls�����E�d�X��jQ,%o�rc���RaH��y3��&w�������rF���an�&o���7�p"��3B���-���\rM-r��*f��1*~���(\E
$���T�V��(?�$�l�eW�x�kr7E��^����go?��9?=�x��KK|S���{�j��F�@F��G!@(NH���E�{^a���������O�0�������F�#
��O���4qk�C���! �%��I�q�y}J~��.�:%x��g��K���;���}3P����%o���o|�8��>�������"����.�r"�k)��(��c���1O���VZ{;%�JgF�D���)-�y����Ty�,;W�w�������c���S-��!�B9A���q������?_\�Z[��f��~�|W�J���z}~�����P &�y�U��H%�����e�O�8'�}����G�_���)�y;��3s��Yp�\����Q���6^�&���@���%����:����b��v�v��A���v��&�%x(��w���b��� ���0`�T�*�
�J#fX9���Wd�v#_�������4���`��H�o��8���
(g��2�6��T/�X cTH�fj�"8:L#WE72���Q�����<$z_�W�����y��,R���Tq�y��	!F�����2<1X�T��}���X����8�W�$l��z�4q�����Y�%a�2��|\��.�$9����sI~�"F��FOH����.-W�����>a	Xq�E�s��1�t��'
4�b{32�kX���o�Y����L���.L�	��su��z�d7�Y������B���?��������%���)�L�0Ol�1z��?&?���V)����%�L�7�5�0lC;N	�kT��;l�jRJI0�}C��,`/#l+��S�S,'K���JB�B0$B�����H�3����&�Nv��
���"��@�Bqm�9n`t��D���7�|KSc2e �S!�`������;���IP��h���
*0����CMt�T0��8�@4mV����9�XT��2#�@������R��-�Ij��6F��,��3��E�S�
���@H��W��*W7b�5��?�������_�tvu������.�.^�}���:,��w� �u��WN�K�_���_�!I=*��,bK������#d^�@��I����(�H���������V���4�}���#������gB������
�����\��l���`�*s�9��Rh����MpC����SJ�� �Nf�F3@F����}�V�'	C#�q�5f�����}����N�4aP�r�>���
�����$>H���������M";E��Zgr�!�l��V'�F��A�G�f��~&��v���������
���`���g�0d�����9�3�^�oi#���@��'K-g w7����Lw[�s.)���Z{�7����"Fap���MQ)���A�����w5P��0�������L�_)���+�f ��7'9���z�ij�,8�D����������:7�����.AF�1�l����8��Z�jj��oy��r5����}@�/�P��
N�v��a$�}$r���1��T�����f��(��)���;��g`P��k���������,6������e����g
o5~�"X@��=>��������1�����:y|�4{aD�4�?4�Ly3N�<B���Dx����6��D�AX�X��a���������
q�su�m������3'��R9���6oOTg�9k�n��FO(G Tl@xn%%�����0'?lY�<�!�I�f<:�R;��;��,�2]��2��d10��1�>�I$������C�<����3Vw�����m�ul �����l�2ta��`�)���������&�.�:�u�����i,P�I�D�2peJl%����c1�������J,H�[(��;2>�8���^_���o������Ly�'��0��I�[������<mO.��A`����G���>��� lJ�|v���	�d���v�j��b���L�+*]./����:���fv �E5�*i,�"��X���m��k�<u�,N��f)�+���~{�hp��H>r��h��82[�2������eJW},��Rk]�}�2Z�)F��y^�Y8�cQ�H���rO���r���D�?I0y����:�-K6�U�f��S'�E|T�A|H�@��"V�u��� Fse��-|�o�TI�y���YP��&<�+������p�������`�/~��@������&P�������T�V�n�V����H��n���N*
cQu��=��CU�7��������������5VLi������6
��d��!0��zP�	(��!��-N��I�p�8��C��8���.��)�y.��R�����(�������1�gl�����`���d����]�i��J{7�/��&r=�@���������Tm@����j<)E�X?^�X=2z�{����$����U��������
�K��8�0��0�����k�_��G*Vn�M�Hq-c���>��G�� J��#�b�����3Z�v�lo��f�Zu�Ah����'m@�������1����!�=(��;������r�D���(�-���B���s�_4��,5�D���W���8��M�]�j���������	��������.�6P`tq��MT�yP�����c��??���V�t[��w�C_��2�{��^�M L���i����}hE-������ �����)Q�E������9�
~�
�f��n����8�����^YM��b�,�Q�UdQ%�q�r�X`A�%1���������K�����+"	�����wo��� ���]����(CHU���M#�~U`���&h�
MM����V!��V������Z��Y�s��Se"��(*�i��������j��1G��_rFC������
���
����!6> ��c�������Qf�nlL���J�6[���Mv��}WI8F�,�x����b88r�:�R{M�m��Jx��D\AbIm���F���
�p-��S^�Qt���t������\ �6����/�Nd�9����+�
W<(����y������#f%������Gu�}���I������\`>�cp>M��x����� �Q�����t�����c����J�^<Q7���G�������s��9�fs�x4�2���6����3m6�LIu���)���t`|��tx�`�)�(�V,�&����9���O����W�&Nr�����#D5�'Oy�Lh?���`�;@��z1�^xZF[}'��
8��
�m��r��>5$W �(�E[��qY��xe��s
����Y+�Nwj#`(\�C
:z��9)�x��t��b�Z��=�()Q���z�^�&a����v�a4�^c��8[f3E���AE����v�yo���
g���w;yu�y������Y�QD�e~��L[���2�=���59Y�X%��u8
���&=�$Q����o g(�;j���K���c�1���C3�����1�N���0���j��/�����`7��L����Q&lY������ �_	����9h������w��+�Y�SI�$e�;��o#�d�Q�bv��d���b��/�:BJTL�}Q[�
^G�g���3K���P����#l@��7��1rn�G��De���O��&1�����j���#���_p�x|�o���6�X>C��s���cP���ZL#�Q�;�@L�b��'���~>��jX&l#�B�����2�wH[�b���
{���`�G�3��>'O.^�U�N�$�@��'���d���Drz��:�U��>�7i�3�d�����k��g��R�������w���>�	P�b�6>G�Jx���������R������� TSg!�ah��G���%���f��e���Sn=����;���N�/��t�y��Ra0��WtiXW������g��b
���A���*�(�����	��'��![��3���\��-���V=����sq=J��eJdv�A�R����tb�Ld|�����+&�7H�8����I���5	�6���J����O���ke��s�/\����*!��/�o�TB�����Y���#�����v�����������MV�i���������&Y��R��
��N��~O�(t���������:�2��%����"�*�E��n��4\�'��c:w
B]��56i��;�!�aPO�T
����	���|^��a5��qkD��L��!q!��������� ��Q�3%�L��sL�&K>?z9�0E��R���~�����A��(����U����.Z��t��=��0��DT�'���]+�	�����32#N�����b#T�����G[[n4��C�e%�j���M�+xDfr\�n�5Iei� ��2���v�h
��,���x_����,�#�i�%���C	�^<!�{���c��EW4f��K��S�	�i�%^E8��;����b��~/lIu�s��f��)<KN������?����n-�;&�yd�n���/Q��9��)��07�Q��(""�#KcZ�(8^H��8OH���s�sj^V�w��=x�p�t�b\pTT4�m��MR`�S��}P�O5���\�!���%��NB�4*r���lS�R���-����h�E�l�����o�B
��n_#�Zny�9�O��;b��NU�����^(��[h<C�e2�$�G�}Z/����%G�G���kx]}>UPeLBkN������<���q@B'f��KA���2�a�~�E!^��/E��b(��C�H��mv����W�Dc�=:<��y���C����k�'�"\���?�*
�C6x'��fP��\b)�Zi�e�y�E�9;v7:::R�<T/�NH���b�@\]Y�:S��C��/�9�[��R��;s8���N�����[a]�)EI�H���@�����#��������/�f�^:�In�V��tLz�H*�Y��i%�����I�<�I�`��l��_�X�<~yz5�.�������x�}�������[�X��n��t�|"9<g��_Prn:(���5@0[[?Lr�)��YG�������~��X\Z��U\k:*�x-�8�)�}�O1��]�G�w�\L�-gw��w���*��mU��g�iL�k.��u-��eW���/�p��y�y)(���eA��b�W����hIO �U#�F�}#���Cd$A_�e��	�L6�������2�1�)��t���X�E�l��_�����p�����xAu/�d��R�|�~u�����9��k.s%�2�I����Y\�s�T�g�o�������	�GaL���1���E�������'�U'9�����X-�u��u8b�,�
yN9m�����_i/,������S=�,W,�%�oW��E���x]�&:�Bq��k�����T�<n\��=�m��1KQ��G��6��O� ������s��-��`%]��Z��q%�p!�(���
�J5��4�QtC���u 0R��9�B;�����|&�7"w?A�cI�C�I�r���$nyd�@��(��.Zd`6l�Nl��6�c�������x�M�@��_!������#����O��+Z-D��QO?�Q�|��.F�M?^{}wk�����E���~�
n-$��Z^��l��������F{�<vh��'f����9���+������8��Dh��#t���/j
�g�=-'���Wn�$(q������k����L0�L}���T7��a����b�Y)Z�9��l�O�����|a�'��d�Q'�&8���������f/�-��j����u[�rm�|c6��'d>^j�N����@E�i��6��|�Zc�O*��2��9��4���D� S��{�Hs�����!
%��8{v��*K���8�3�Y�/6X�r��6�^a�n*���@�O�\��X�m7�H��7f"O���S%�.�AAx	s��m���m�a{�����o�C�g�pwtBq���=�rPh���G�w$O5�6�����W+H|�4�k��mA�2�����=Amu=�!���}�y����sza�]������.���0�8��'�*�=10g9?�����35+8x,q��e9�=��{�z=@�{��}�f?)�$� ����!�9j�,oc��i����/��#`�;)��]�+���3���7"�;�S�����N��������vh�(*���%�+f�#�f�s�9�AZy����%	�"e
�.�U�w���B��OE7'����
ac�W�����+����,B��p�����@||@M��3���s���,]�����W��%�(����=�q��P��q��:��AOTJmo8����-������������#'�|��j .�l�B���Zn6OM�fy�����%K���#��8t�j>r�nrD��1��{��E@����7����d�u����A+s��������e�|�s����+����"k�}�n�G6�LQ��;�������<�>�
�/{�a�#��Oq�}9�8'�C
t�[�C-�&�e���^�������8X���p���������T;��vL���������}W�\��5��:�N�����dA��C�y
�R�����o�|4\]�;$:n,\�`�:�YM������Z�W��j4��3�tH�����������e4���F�����n�N�Vi��������|K�F�8��},�99I���y��4}�[��^����=Py�����z�'2�K$�����lP\���H)�S�6�[C��k�X�g#sH������u$x���6��(����UN���K�S�-'���-��i�v���F�/e�lD�zBZ�����
,�q��zhP}����1gs�������}�X��Q{�E��� 
�n'�������K��
��o����(��r���c#�M&��U��"����<�y���\��x?��u:��qj��3�Jp�v����H�gk�VK/��/����_�m�F'�4�[X����L
����R.q"U_g��V���{#j����V��b�\Rib���I�AL9��83�P �`,t��\����)����m}i��,���o|�9>�qv5_.u
!*]K�j;����@U�}@Q�����2	�����B
w�������b7�n��q����.��t�/r�D���Y��J6����������.������+	t&�8iz�`cWW3��U�oU�#��ZF�U~�1ql����P��/�{�z����u#0�����~�3�_�����l�����_���t�B���{	G�J�t�N D�����_�<3��J���s���s��$����|&�q����R���tDM�VCL���nRj�![)�\P��\��>�U�"��F�*��$����
�]��3��R%a�7b��o6[���\\w��j�6C�X-t0�L�P�
K���]��E�WPQ��-.W�t �v���]�V���%���V��Y��J�<3r[Uo�;��rd����M������j�\���vN���c��>�Q����8�o����z���Z|8��@x{eG���1�����l�������k��7y����'4E��XoV�t<��{��d
7��}M5]���}1�|�8�9qu�p����x"�2��H3��	aP��e����s��A@3��H�(�|���e�P����H�6�����R&M6�Ss�y����6n]��I?9��W+}�\JZ�z�y
@����S���������C����#�M����;K���U����.D�=:�c���<������?Bz_L���������X�+���@�-3�N9f)I'v�4��+@���BT���V���&�qCa�;L��Bs&P�\��Zy(�e�_��c�wt���j�_V<(��K&xx��0,<�?����H%a�����������^�r������t�n7��BJ9����*�gk�
Z�C�8�z��h��:z��<@=V�Ie���yQE��W2��I�w����p����_�pb�	�k������esPd�
)������-�3JN��pp��s�)�*}���q���R'I�D���YY�^a��B�\=
���#`�;���
m��U0��='���V�Sp��4�[;OhC��O��7a�L .�����SC��Z��5���3^H�N��I�\��2>��B~$��U>wT:�:i�N�SO2s�]#Q�eQ>=j�T:
���(H����#�6�nY#{��k^Bz��)����O���5�����p����{~q�R�Mi�_b���J�\a�%+:L�bh��bw3�F���o`���I���1�K��a�]�8<��i��A(�����;��:��j�W��>�e����,pA`�u*�\�����
*)i$����KV�+�e����k $�1���:�����4��s2]w������'�2�G�E������c���.c��1��-��
w]�X��6�+���[�[��������8I1&��7":9���5�I��������hq���m��$�a	~ ��L���v���`���f�?\"���Fx$�B��|)7kN_�o=4:��������Q67��)�)%*[��+�: ��������P�S��="�����b����P��j��I�rk=��X���<f��e�[��3�o��PV���V�7fV"<��(��
P�u}ue��~����D~�C���$�qx@����=���DIV"��f����eAvm����x;)f�������n6��<���S�Y�����"��s[\S_8�����z�.�N�=�;'��p|�����{�F�:!E8$���?��Z�g+���8����`0[c���
������i,h��dr����J�s��J����yxI�o{I8��|��CT���fqD�/��)�<�9Zm|kI���i�{��{�������6���DJ�d�.��k�,ZGE_3��J��"�)Vl�r+CJ@�6���#����=���@��W5}c��b['�F4�:K=)/���n��O u��$�rX|����{x�;���@��-'�M��E�@jQ�:u����Y��F��)|I(���� ����/{V!�Q��aJ�F����Y-}ND<x@�S����3�I�x
N������~��������WO�j�Y��{�N�M��YR�;L��"<��&�����.����D2��+���.�9B
c��#Y����$m'�����^��N���%!�t�V��4���h4e��0��2b�q6���E���9�
=�Ng(|�F��.I�h�d��y�c��).���q���#��<�<e����/_�=��e��w����K�U ���jng���s%:]���;�wg!(���mUW���W�����Y�������6cg�
���>1�]�w
�����zJ"�n�z������]
���k�)�Ymxg�|��n�oR@z�;���f�i�2���K�n�����o��]5�6�@P������b��g
%z���m�f*8=��qrb��zKYS�!���n�~N}�L����-�{o\�������j���s�Z�K�����~|��������7�����vg�	�8�pcRr:����7��79�S��u}�i�H�6_�>�.(�o�$�J�)�pgL�����(C #�ly	�
�^,��u6��|-R]�PR�}�������h��v�����@��? t~��������=�f5�\��{�X�0���%�5z1R�&��D��y(vk��)_�t7k�P�*����.����x��%����y~���W���^��x��M���:�*�dn�}-+�����Jpb8�L������
����T����������g��6Yi�E/�"`���[ZA$����D��o��/������
���ON������f�'R�QDV�����E��m����=F����	����FE'�Q�#�I����=�������{wr0P�i���a�;u��!�7��[��������f0P��$���$=s6� w6�����
��J�f����o���f��������������<Bn��>;n���l��Pk���O�m��.����5�o��h�:�g�?������U���Dk�\b����!���#�"X����8�����N�
�C���Y���������5�Y@f9�:��f�3�z�<��yz�i�B\�"�O���%�����9�E�u`Qd�����q��`.+�����`�=�~������h�/��N��3T�;�Zm�P�Z�e"UYEK(
�����������`���U'���h��6)B�Y}�_��9��T+_?�B�d=�{���<�@I�!����bJ�X�9B�.����t2�Qwn���	���B��k$�?�}\����AQ=�&�if<���"~�6A�8;�#���%��)��X����0��v�f}�K��E��7�����/��n�p[���z������V�����#�I�[�1�[P����u��\�|�
s�aAs���P��x�!)�n?��Q�lH|(����G����Qo04�`�/}iN��	�B_t+���;y���eJ�z�7�� ��7T/F�o\@.�>g���}S%�&aw:�=Y]��h��@�����8���c�^���xA��j;�J���H��V��DGy��
I�7N	%�Q-��!�A�A�ND�nWR��I%A0�%:Ht��u�3�Mh�g��""m���gu�n���S>
��-�"KG���6������"�(�FNig�����������5������} Q���%[h��\�H��\W����z���W?�|r���*�;d4��6S�[�M?�2�]��)�/�G�C��w0�=��+���������~3�p�.�����m������&8|�d'������8B���� �|�2���pd�U��C���1��i���[
������8���y/h$��j�&���O(o|T��s\���],4��$�M����:B�V������m��G*q1�y�����������1kV��t9�1��S�U������4R�^��(9����I�B��,�*;��f���c'ip�M9��o�P��l��&���tfQ�l�@h���x>�Z�2�7��m��k�@�[�iM�\v�����&�������\
�V�&�5K������,������������O
V�W�}�#$��
����,�v�"8���/Ai�-��x��:��w�Nf�'Y����C:!�5����V`���1o>Pt���'^�������/���+��%�E�S#y����z���ki�>�l�U��:+z����l�XT��r���Z��O{P[�i�O���`^�iw�f�K�G�?������G/������������$;���A��W�t@e�t�~}s���L��'j���������O����rs�f=�U��M���E���7����9�Y���418���
�m-�$������G��|q-�H�@����o��Y���E�|���G�|�� ��d(���B��k����f-�g�Q�Y`��A�����|m%�#���E�&29����*6�:��$�F��%�l�����t7}L�X��g��H���w�����J�s�vua';[p�E�`(>��Lfw�e���e��6p�+s�M�I�4�}�Y���6�>Y��u@�P�� ���0��z��:\�
�d���*�Z�bk/y? ���@'�!��K�dSkv)���E&�������?��&�����r�_��|�V{���4"��G 3=�'9kUdo'z���/i|`���[l�}������|�w$jP�2�q�3���]s�������&v�DT������Z��l��E��bV���r�k�b���"���,]0+�����E}��r��g�@�l�����1�f��\�@����f=�&��6���|���}�L��Z�#~�3?o���"O���q+N������O�%H���M��Dv�
�b00H���e{��PX����l��7�U��>Q�i�uvf�q0���"��j�M����7���!WIi�F��Yg�fzo}~m��B�>V�^�)�R�Yr���>�Lys���C���R�Ix����83*�tf���T4�<���������c1�p����WL�s����&��W�_YM�5��9=w�X]���ys�����/��'�_L�u�7i������%'�w�"�X��d�I#i��-�)��oq�g�$Q�2����|�_b�5f2�������?��Tw]{yxX�^��@�o13kC(�\O��[gN#0[��V�6�e����v�F��G�������E��q����6dR+��;$��nB����S����y
�����<b�9|��u����4�\_c)Pm�q��@��QD�d��C��#&W7����������\��
?~�q���~9�Yd�d.�m�����7���?���7�\p�}���n���LW���
��(7��r9�Q�]>����=1T���f��q��5�c�4�����_�:��<�{�,����Z��l�6$�����yU�
���<��*��o��I�R|��-fR��DDh�'�{��S,E������N=���k�L����)W)�tSW�f�����B.<��T��b�}'Nh zb|�|I9�4�����qa1_��S��O������	�A
;�	6�h�����C����`��`{���u���/�������������d����(��mX�\����<�]����\�gW�1��J�q��8��c���n�G�����v,H��e���i4����iY������o�r�Jd)�b/V���j�	
5���<+�e���s�+�i�k������;�k��}R"�'D��@B)Z
j�4J���Y�@�)
��]�v����NmWv��k'2�*�KT�*Wm�t�5Q)|Li,�[Gl��E~<��&��mS�u���?����� ';i���*K�u��o~s���������[���$��$�������I_�7�����[�f:�j�R�,��F�=��CP�R=�T��v�Dl�����;
r�8�d��]����/�D=���5���P�=��yN�D�`��7�:���9Z�;����vl��������E��*��Z�I�������]f�X�9�������R-��=���*�&����qh
��_�\J��� P����Z+Q�e(��h��J�R] uV����'�����!��1���s2��A�d�
�fws,G���bG���X�Vy:��H����������.�����8���
u�Y
G�����s�x��9F
E�Q�w�EW@��FK��uy=����!���Sl"h��1��y��?�29�.o�����/��D�����mx��Y�������f"�/A�o��Z�R�?�"�S�_��Y��i��w����X�v,�wv�~��r�UD�%�����������)W���f
�j���TBL��U��j&��s�N3L�����Y�e�Zk�)��V�&���3�H��2B���9c%���X��|��c�N��~
r�wNrl�h0��2��:[���sO��PM=u��Hg>�1�������)RA�������Q0��c^y:��
u�I���6�+����!���E���,Q�����
�����1Hh���-�����2�V�Yz=_�
�����D��R�CP����o6�K���B����-Z��w����iX�}������7����uo��(���T�j��}���5re����VV��X��E���Z{
uy�pG�R��7�-�Z���i����7�p�4�����M?r���c���>�
"��,e�m���O��1���F��Ft�^��5x'�OC����G*�3�+������1	�`
U����k�W��j�q3S5�~�Kez�����[c`|�A/^AI������|9Y�L���t�*x�<z�����p�Q:f���l6=g�(�v������a�{����G�'���0:��^�>yvzq��d��V�;���^���^7�E�Vk�U�K�n����J{$�#4���9z
z�����q�w[�!��u�hqOuV�hq�K��7`Y��t�7��H�n�N}�4h�i ����bD<:�����:��^���G�Q�N}�����)��)1Qy���^}�%#|d.�$�4�Ic�I�N����I-��mt��Z�th���I��j�&����b�:(T�?SW_�a�c�m��:�]W_��q�Nr)��OV_�AR�R��A�/� �})�&�#�Z��@�Nj�;q���E���I���KQ�X�Z2�_
��3��o2�����3��������'K����G��*���J{9��A�*3���mV�c�] �9V��c�;��g�Y=9f5���8��B��F
�
��tz�c��{��S_f�b[�N�����\A�W��0�,��%?�Mi�"��v*Mi�"�d��1�$�(
�w4�����	�'E��������[�\V'�`2�D.�S_f�D.��@:#�E�"������JU��S�J;��S�,��l�mJ�������w��t�U���y��Nr!}�0�7"�
;j����bJ?�C_0*�$w��J;��>����Nr��}�����p�(��A5�K;���@�Jdp:	���0��@��G�:��3n-��
�%�� ��7����z��m���*���$���h�N�*���$�����N�*����9�a�:��TD@J;��NE��S1�Am�=�&!�]���N���:!���	�^�W�]=����O�0&>�W�K�_�n����~�Z��@�Uq(Z�����N������q�����������z���~���Da�__�Ub��o �*	����.I�����~
�	������/��o ���s���R%���I���%��B+���K�
&��7�~zh���b�%�Vin�~=�Wo�_"�������~
�7�~����,������/I�\,����S�Q�\,���+�6���G�G�~�A��.5���'p���������2����&}ytwT���<�;�I_F����~�Q�s'f���J���O�n���'t�J����[���	��78ws���d�%L`U�"&��_X���	4�8xz���7��3�y&{f+�s��a�<`>"�J'*��FC��������/�~�~
n����Rz����+�#�����3�1+���������_����������a`�n��h�s|2$L�`���'�.�Cx}���T�K�_&z����
�~���������6q�e�*�sR`U��V��`U��f�P ���aRD�0,�CA>�C�1{��������Z�#[t�����q�?5�x�
W6n����E��l��������x�zj5L����a�a��WX]��u<;T1��'
r�����n��a��&�0�n������q��U#�u7��L\w3q���u7���LRw3I��$u7�x�	7���M\�l��g�=��9��5��5q�[��5q�[;��j�u-��hq]D��"Z� Z\�hI��$u����NRw;������ ZR������E��A�������.	H����.	H�S���/l���~a��8���
{�p��t��8)t���
�Rbd�0'a7,�M��������Mr�I������8v�^����K���Y{	^��1�����������?qv���0����b�!pmB����������������_�d����G��K������G��K��^cq'��G���N�������kk"�����O:J���`�8���m�){m+�
�����E�y�m���o[v�~�������n����x����wW���mk�.����������.n����]����jbf�3��7����L�;�M%���_���)��%�}��?9��T"'dk�N�B+��-W��m+�R�� h�=��X��-�
�m9V�m���[��%mG
�6j��Q����mTo`���7l[so�����m���z+h��Ld�On���F
Nn���F
Nn������Q�7jp�F
n��V�`��V�`��	V"1��;��zw�m��L���Y��<9zh�a�i[so5�������Xi��{�GM������Qk���o�p����m������:���?�s[q�x��-�����|���T�����{)A����T�{)A����Tr��j�kD�zyw�������(#
�F%�y�����Z�(^�RD���"���QJ���F
��o[��Ir)��m���_/(��Fsw�%(2�w�Q����s=��u��O���lvt4�w��q=����ln�~����D��7�#88�&���%T��b��W����������EO�Q4�&���u&7�C���^�b�'�C�,"�fe#�9�l`���E����!(
�CX��^�M�S��~�zU{�a/���l/1�$._I�^�G�W�%���Fn���$�� u6����bH(U����%H��%�8�fe#������!X��������cX�����T���X9`#A��p#`F�u�?�B�d�R����@-�-Z�;N���X�I+Y�`���3��zo1������T�-�E��joU���-He���@=�\�r���<�L����@=�]���;qR���������U�������=>F
H)�(���&2��|�JI!��i9A�17N!Z��Ik*�U��Q�a��j�/�;�=w���@yC�nF����V�P��C��V��mtTc�f����h}a)7��lh��ca&����V�PC�i����*<�~�#��HH.��0���N�$�M;����Yo�c���U�����1�`/�(�C��Gruq�
T|��rP�H����^Yo�B,c5������!>�D��*�J�:�?�&�d���s,ouM��� ���������"X��1@"I$c���x��\5UY�n�������%�r�����8��.��t,=���G}��R�����������@���q�����Z�T�����c���g.1����c�}�9�s���~����<�r��=����t�.�:d�
��mjx�m
)��<�"^C���`+ �����B`(�S�Hm�M�T��~n)�7��Z.������Z�
~���B��cX���!,�OtX4F�#MF����g����g��;@��"�9��\_���f5nwY�Q��K����J��/@�L�F����7�^'���pb��gma��h�@�����j��M������<���J�P�������D�E'�^w�)x�Q��)yoM�I�XP��a[!��7Q��bj���:ti�"����r�!��������V�l:�I7������$�fY\����ik&C,=�?z@��?}������Wb��Bf��pBg;�K�cwD1���3�o��?��Q�>����^O����^t	��.K�&
7�.�h�/�J�h5%��=~cu��[��q��>k����[?(�zx������
���a?x��w���x�Ip���r;����?�s���n����!���n7�������i�����~�>����w��^C�)"g��Y��P;��Z�������C���;`�+����rF(0Fw�Q%0��� ��
������U@�{\"�?"w�!<XDN�!�:�?$��x70����Q��@�A��?
"Oz7���* �T����8P����������%�i����$-�H�0J���,/�'�''�MY���?�zG'G�������$1w
0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzapplication/gzip; name=0004-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzDownload
#11Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Tomas Vondra (#10)
Re: WIP: BRIN multi-range indexes

This stuff sounds pretty nice. However, have a look at this report:

https://codecov.io/gh/postgresql-cfbot/postgresql/commit/2aa632dae3066900e15d2d42a4aad811dec11f08

it seems to me that the new code is not tested at all. Shouldn't you
add a few more tests?

I think 0004 should apply to unpatched master (except for the parts that
concern files not in master); sounds like a good candidate for first
apply. Then 0001, which seems mostly just refactoring. 0002 and 0003
are the really interesting ones (minus the code removed by 0004).

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#12Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alvaro Herrera (#11)
Re: WIP: BRIN multi-range indexes

On 01/23/2018 09:05 PM, Alvaro Herrera wrote:

This stuff sounds pretty nice. However, have a look at this report:

https://codecov.io/gh/postgresql-cfbot/postgresql/commit/2aa632dae3066900e15d2d42a4aad811dec11f08

it seems to me that the new code is not tested at all. Shouldn't you
add a few more tests?

I have a hard time reading the report, but you're right I haven't added
any tests for the new opclasses (bloom and minmax_multi). I agree that's
something that needs to be addressed.

I think 0004 should apply to unpatched master (except for the parts
that concern files not in master); sounds like a good candidate for
first apply. Then 0001, which seems mostly just refactoring. 0002 and
0003 are the really interesting ones (minus the code removed by
0004).

That sounds like a reasonable plan. I'll reorder the patch series along
those lines in the next few days.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#13Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#12)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

On 01/23/2018 10:07 PM, Tomas Vondra wrote:

On 01/23/2018 09:05 PM, Alvaro Herrera wrote:

This stuff sounds pretty nice. However, have a look at this report:

https://codecov.io/gh/postgresql-cfbot/postgresql/commit/2aa632dae3066900e15d2d42a4aad811dec11f08

it seems to me that the new code is not tested at all. Shouldn't you
add a few more tests?

I have a hard time reading the report, but you're right I haven't added
any tests for the new opclasses (bloom and minmax_multi). I agree that's
something that needs to be addressed.

I think 0004 should apply to unpatched master (except for the parts
that concern files not in master); sounds like a good candidate for
first apply. Then 0001, which seems mostly just refactoring. 0002 and
0003 are the really interesting ones (minus the code removed by
0004).

That sounds like a reasonable plan. I'll reorder the patch series along
those lines in the next few days.

And here we go. Attached is a reworked patch series that moves the IS
NULL tweak to the beginning of the series, and also adds proper
regression tests both for the bloom and multi-minmax opclasses. I've
simply copied the brin.sql tests and tweaked it for the new opclasses.

I've also added a bunch of missing multi-minmax opclasses. At this point
all data types that have minmax opclass should also have multi-minmax
one, except for these types:

* bytea
* char
* name
* text
* bpchar
* bit
* varbit

The reason is that I'm not quite sure how to define the 'distance'
function, which is needed when picking ranges to merge when
building/updating the index.

BTW while working on the regression tests, I've noticed that brin.sql
fails to test a couple of minmax opclasses (e.g. abstime/reltime). Is
that intentional or is that something we should fix eventually?

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patchtext/x-patch; name=0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patchDownload
From bbaaf2dd5ac6fcfff37b226afa1197b02c48ce8b Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 9 Jan 2018 00:59:09 +0100
Subject: [PATCH 1/4] Pass all keys to BRIN consistent function at once

When building the bitmap, bringetbitmap was passing the scan keys
to consistent support procedure one by one. That works fine for
the simple default opclasses (minmax and inclusion), but not for
opclasses with more complicated summaries.

For example the multi-minmax opclass needs to see all the keys to
efficiently eliminate the partial ranges, otherwise it's mostly
just a more expensive minmax opclass.

So instead split the scan keys into per-attribute groups, and pass
the whole group at once.
---
 src/backend/access/brin/brin.c           |  97 +++++--
 src/backend/access/brin/brin_inclusion.c | 484 ++++++++++++++++++-------------
 src/backend/access/brin/brin_minmax.c    | 168 +++++++----
 3 files changed, 451 insertions(+), 298 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 68b3371..b568786 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -367,6 +367,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -388,6 +391,53 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -448,7 +498,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -458,34 +508,26 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
+					Oid			collation;
+
+					/* skip attributes without any san keys */
+					if (!nkeys[attno - 1])
+						continue;
+
+					bval = &dtup->bt_columns[attno - 1];
+
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
+					 * Collation from the first key (has to be the same for
+					 * all keys for the same attribue).
 					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
-
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
-
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					collation = keys[attno - 1][0]->sk_collation;
 
 					/*
 					 * Check whether the scan key is consistent with the page
@@ -497,11 +539,12 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					 * the range as a whole, so break out of the loop as soon
 					 * as a false return value is obtained.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
+					add = FunctionCall4Coll(&consistentFn[attno - 1],
+											collation,
 											PointerGetDatum(bdesc),
 											PointerGetDatum(bval),
-											PointerGetDatum(key));
+											PointerGetDatum(keys[attno - 1]),
+											Int32GetDatum(nkeys[attno - 1]));
 					addrange = DatumGetBool(add);
 					if (!addrange)
 						break;
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 2542877..fb7f2b0 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -248,7 +248,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION(),
 				subtype;
 	Datum		unionval;
@@ -256,235 +257,300 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Datum		query;
 	FmgrInfo   *finfo;
 	Datum		result;
+	int			keyno;
+	bool		matches;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+		ScanKey	key = keys[keyno];
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
+		Assert(key->sk_attno == column->bv_attno);
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-	/* It has to be checked, if it contains elements that are not mergeable. */
-	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
-		PG_RETURN_BOOL(true);
+				PG_RETURN_BOOL(false);
+			}
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
-	switch (key->sk_strategy)
-	{
 			/*
-			 * Placement strategies
-			 *
-			 * These are implemented by logically negating the result of the
-			 * converse placement operator; for this to work, the converse
-			 * operator must be part of the opclass.  An error will be thrown
-			 * by inclusion_get_strategy_procinfo() if the required strategy
-			 * is not part of the opclass.
-			 *
-			 * These all return false if either argument is empty, so there is
-			 * no need to check for empty elements.
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
 			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
 
-		case RTLeftStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverRightStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTOverLeftStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTRightStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTOverRightStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTLeftStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTRightStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverLeftStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTBelowStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverAboveStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTOverBelowStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAboveStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTOverAboveStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTBelowStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTAboveStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverBelowStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+				continue;
+			}
 
 			/*
-			 * Overlap and contains strategies
-			 *
-			 * These strategies are simple enough that we can simply call the
-			 * operator and return its result.  Empty elements don't change
-			 * the result.
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
 			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		case RTOverlapStrategyNumber:
-		case RTContainsStrategyNumber:
-		case RTOldContainsStrategyNumber:
-		case RTContainsElemStrategyNumber:
-		case RTSubStrategyNumber:
-		case RTSubEqualStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													key->sk_strategy);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
-
-			/*
-			 * Contained by strategies
-			 *
-			 * We cannot just call the original operator for the contained by
-			 * strategies because some elements can be contained even though
-			 * the union is not; instead we use the overlap operator.
-			 *
-			 * We check for empty elements separately as they are not merged
-			 * to the union but contained by everything.
-			 */
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
 
-		case RTContainedByStrategyNumber:
-		case RTOldContainedByStrategyNumber:
-		case RTSuperStrategyNumber:
-		case RTSuperEqualStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverlapStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+	/* It has to be checked, if it contains elements that are not mergeable. */
+	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
+		PG_RETURN_BOOL(true);
 
-			/*
-			 * Adjacent strategy
-			 *
-			 * We test for overlap first but to be safe we need to call the
-			 * actual adjacent operator also.
-			 *
-			 * An empty element cannot be adjacent to any other, so there is
-			 * no need to check for it.
-			 */
+	matches = true;
 
-		case RTAdjacentStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverlapStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
 
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
 
-			/*
-			 * Basic comparison strategies
-			 *
-			 * It is straightforward to support the equality strategies with
-			 * the contains operator.  Generally, inequality strategies do not
-			 * make much sense for the types which will be used with the
-			 * inclusion BRIN family of opclasses, but is possible to
-			 * implement them with logical negation of the left-of and
-			 * right-of operators.
-			 *
-			 * NB: These strategies cannot be used with geometric datatypes
-			 * that use comparison of areas!  The only exception is the "same"
-			 * strategy.
-			 *
-			 * Empty elements are considered to be less than the others.  We
-			 * cannot use the empty support function to check the query is an
-			 * empty element, because the query can be another data type than
-			 * the empty support function argument.  So we will return true,
-			 * if there is a possibility that empty elements will change the
-			 * result.
-			 */
+		attno = key->sk_attno;
+		subtype = key->sk_subtype;
+		query = key->sk_argument;
+		unionval = column->bv_values[INCLUSION_UNION];
+		switch (key->sk_strategy)
+		{
+				/*
+				* Placement strategies
+				*
+				* These are implemented by logically negating the result of the
+				* converse placement operator; for this to work, the converse
+				* operator must be part of the opclass.  An error will be thrown
+				* by inclusion_get_strategy_procinfo() if the required strategy
+				* is not part of the opclass.
+				*
+				* These all return false if either argument is empty, so there is
+				* no need to check for empty elements.
+				*/
+
+			case RTLeftStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverRightStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTOverLeftStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTRightStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTOverRightStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTLeftStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTRightStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverLeftStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTBelowStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverAboveStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTOverBelowStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTAboveStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTOverAboveStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTBelowStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTAboveStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverBelowStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+				/*
+				* Overlap and contains strategies
+				*
+				* These strategies are simple enough that we can simply call the
+				* operator and return its result.  Empty elements don't change
+				* the result.
+				*/
+
+			case RTOverlapStrategyNumber:
+			case RTContainsStrategyNumber:
+			case RTOldContainsStrategyNumber:
+			case RTContainsElemStrategyNumber:
+			case RTSubStrategyNumber:
+			case RTSubEqualStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														key->sk_strategy);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = DatumGetBool(result);
+				break;
+
+				/*
+				* Contained by strategies
+				*
+				* We cannot just call the original operator for the contained by
+				* strategies because some elements can be contained even though
+				* the union is not; instead we use the overlap operator.
+				*
+				* We check for empty elements separately as they are not merged
+				* to the union but contained by everything.
+				*/
+
+			case RTContainedByStrategyNumber:
+			case RTOldContainedByStrategyNumber:
+			case RTSuperStrategyNumber:
+			case RTSuperEqualStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverlapStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				if (DatumGetBool(result))
+					matches = true;
+				else
+					matches = DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+
+				break;
+
+				/*
+				* Adjacent strategy
+				*
+				* We test for overlap first but to be safe we need to call the
+				* actual adjacent operator also.
+				*
+				* An empty element cannot be adjacent to any other, so there is
+				* no need to check for it.
+				*/
+
+			case RTAdjacentStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverlapStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				if (DatumGetBool(result))
+					matches = (true);
+				else
+				{
+
+					finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+															RTAdjacentStrategyNumber);
+					result = FunctionCall2Coll(finfo, colloid, unionval, query);
+					matches = DatumGetBool(result);
+				}
+
+				break;
+
+				/*
+				* Basic comparison strategies
+				*
+				* It is straightforward to support the equality strategies with
+				* the contains operator.  Generally, inequality strategies do not
+				* make much sense for the types which will be used with the
+				* inclusion BRIN family of opclasses, but is possible to
+				* implement them with logical negation of the left-of and
+				* right-of operators.
+				*
+				* NB: These strategies cannot be used with geometric datatypes
+				* that use comparison of areas!  The only exception is the "same"
+				* strategy.
+				*
+				* Empty elements are considered to be less than the others.  We
+				* cannot use the empty support function to check the query is an
+				* empty element, because the query can be another data type than
+				* the empty support function argument.  So we will return true,
+				* if there is a possibility that empty elements will change the
+				* result.
+				*/
+
+			case RTLessStrategyNumber:
+			case RTLessEqualStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTRightStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				if (!DatumGetBool(result))
+					matches = (true);
+				else
+					matches = DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+				break;
+
+			case RTSameStrategyNumber:
+			case RTEqualStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTContainsStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				if (DatumGetBool(result))
+					matches = (true);
+				else
+					matches = DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+				break;
+
+			case RTGreaterEqualStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTLeftStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				if (!DatumGetBool(result))
+					matches = (true);
+				else
+					matches = DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+				break;
+
+			case RTGreaterStrategyNumber:
+				/* no need to check for empty elements */
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTLeftStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = (false);
+		}
 
-		case RTLessStrategyNumber:
-		case RTLessEqualStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTRightStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
-
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
-
-		case RTSameStrategyNumber:
-		case RTEqualStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTContainsStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
-
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
-
-		case RTGreaterEqualStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTLeftStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
-
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
-
-		case RTGreaterStrategyNumber:
-			/* no need to check for empty elements */
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTLeftStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		default:
-			/* shouldn't happen */
-			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+		if (!matches)
+			break;
 	}
+
+	PG_RETURN_BOOL(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 0f6aa33..e58fd16 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -147,86 +147,130 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION(),
 				subtype;
 	AttrNumber	attno;
 	Datum		value;
 	Datum		matches;
 	FmgrInfo   *finfo;
+	int			keyno;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
-	switch (key->sk_strategy)
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		case BTLessStrategyNumber:
-		case BTLessEqualStrategyNumber:
-			finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
-												 key->sk_strategy);
-			matches = FunctionCall2Coll(finfo, colloid, column->bv_values[0],
-										value);
-			break;
-		case BTEqualStrategyNumber:
+		ScanKey	key = keys[keyno];
 
-			/*
-			 * In the equality case (WHERE col = someval), we want to return
-			 * the current page range if the minimum value in the range <=
-			 * scan key, and the maximum value >= scan key.
-			 */
-			finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
-												 BTLessEqualStrategyNumber);
-			matches = FunctionCall2Coll(finfo, colloid, column->bv_values[0],
-										value);
-			if (!DatumGetBool(matches))
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		attno = key->sk_attno;
+		subtype = key->sk_subtype;
+		value = key->sk_argument;
+		switch (key->sk_strategy)
+		{
+			case BTLessStrategyNumber:
+			case BTLessEqualStrategyNumber:
+				finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
+													key->sk_strategy);
+				matches = FunctionCall2Coll(finfo, colloid, column->bv_values[0],
+											value);
 				break;
-			/* max() >= scankey */
-			finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
-												 BTGreaterEqualStrategyNumber);
-			matches = FunctionCall2Coll(finfo, colloid, column->bv_values[1],
-										value);
-			break;
-		case BTGreaterEqualStrategyNumber:
-		case BTGreaterStrategyNumber:
-			finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
-												 key->sk_strategy);
-			matches = FunctionCall2Coll(finfo, colloid, column->bv_values[1],
-										value);
-			break;
-		default:
-			/* shouldn't happen */
-			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			matches = 0;
+			case BTEqualStrategyNumber:
+
+				/*
+				* In the equality case (WHERE col = someval), we want to return
+				* the current page range if the minimum value in the range <=
+				* scan key, and the maximum value >= scan key.
+				*/
+				finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
+													BTLessEqualStrategyNumber);
+				matches = FunctionCall2Coll(finfo, colloid, column->bv_values[0],
+											value);
+				if (!DatumGetBool(matches))
+					break;
+				/* max() >= scankey */
+				finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
+													BTGreaterEqualStrategyNumber);
+				matches = FunctionCall2Coll(finfo, colloid, column->bv_values[1],
+											value);
+				break;
+			case BTGreaterEqualStrategyNumber:
+			case BTGreaterStrategyNumber:
+				finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
+													key->sk_strategy);
+				matches = FunctionCall2Coll(finfo, colloid, column->bv_values[1],
+											value);
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		/* found non-matching key */
+		if (!matches)
 			break;
 	}
 
-- 
2.9.5

0002-Move-IS-NOT-NULL-checks-to-bringetbitmap.patchtext/x-patch; name=0002-Move-IS-NOT-NULL-checks-to-bringetbitmap.patchDownload
From ed8b14643a8c313767f8285b40b44f96a9b8ece3 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 4 Feb 2018 14:27:42 +0100
Subject: [PATCH 2/4] Move IS [NOT] NULL checks to bringetbitmap

These checks are exactly the same in all BRIN opclasses, so it makes
sense to move them to bringetbimap() and evaluate them there. It also
allows us to handle some ranges without invoking the consistent
support procedure (e.g. when the key says IS NULL) but the range is
marked as not containing any NULL values.

Note: It may also be a good idea to first process NULL keys for all
attributes on a given page range, and then only proceed with the
remaining keys if it's still needed (when the page range is not
eliminated by NULL keys on any attribute).
---
 src/backend/access/brin/brin.c           | 116 ++++++++++++++++++++++++++++---
 src/backend/access/brin/brin_inclusion.c |  62 +----------------
 src/backend/access/brin/brin_minmax.c    |  62 +----------------
 3 files changed, 109 insertions(+), 131 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index b568786..7786f58 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -367,8 +367,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -393,10 +395,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -418,14 +423,12 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 							  keyattno - 1)->attcollation));
 
 		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -433,9 +436,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -530,6 +547,83 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					collation = keys[attno - 1][0]->sk_collation;
 
 					/*
+					 * First check if there are any IS [NOT] NULL scan keys, and
+					 * if we're violating them. In that case we can terminate
+					 * early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					{
+						ScanKey	key = nullkeys[attno - 1][keyno];
+
+						Assert(key->sk_attno == bval->bv_attno);
+
+						/* interrupt the loop as soon as we find a mismatch */
+						if (!addrange)
+							break;
+
+						/* handle IS NULL/IS NOT NULL tests */
+						if (key->sk_flags & SK_ISNULL)
+						{
+							/* IS NULL scan key, but range has no NULLs */
+							if (key->sk_flags & SK_SEARCHNULL)
+							{
+								if (!bval->bv_allnulls && !bval->bv_hasnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * For IS NOT NULL, we can only skip ranges that are
+							 * known to have only nulls.
+							 */
+							if (key->sk_flags & SK_SEARCHNOTNULL)
+							{
+								if (bval->bv_allnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * Neither IS NULL nor IS NOT NULL was used; assume all
+							 * indexable operators are strict and thus return false
+							 * with NULL value in the scan key.
+							 */
+							addrange = false;
+						}
+					}
+
+					/*
+					 * If any of the IS [NOT] NULL keys failed, the page range as
+					 * a whole can't pass. So terminate the loop.
+					 */
+					if (!addrange)
+						break;
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed. If
+					 * there are no regular scan keys, we're done - the page range
+					 * matches. If there are regular keys, but the page range is
+					 * marked as 'all nulls' it can't possibly pass (we're assuming
+					 * the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
+					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index fb7f2b0..6d51c71 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -259,63 +259,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Datum		result;
 	int			keyno;
 	bool		matches;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -327,9 +270,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		attno = key->sk_attno;
 		subtype = key->sk_subtype;
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index e58fd16..4621bea 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -156,63 +156,6 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	Datum		matches;
 	FmgrInfo   *finfo;
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	matches = true;
 
@@ -220,9 +163,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		attno = key->sk_attno;
 		subtype = key->sk_subtype;
-- 
2.9.5

0003-BRIN-bloom-indexes.patchtext/x-patch; name=0003-BRIN-bloom-indexes.patchDownload
From a67c77d7754de938f90fbd60044c5c0fcccc6395 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 9 Jan 2018 01:00:39 +0100
Subject: [PATCH 3/4] BRIN bloom indexes

An opclass implementing BRIN indexes with bloom filter summary.
Naturally, this opclass only supports equality searches, but that
is fine for many use cases (e.g. it's uncommon to do range queries
on identifiers or labels).

It's not quite clear how to size the bloom filter, or what to do
if it gets too full (more than 50% of bits set). The patch picks
some reasonable defaults, but perhaps we can improve this based on
more information (ndistinct from pg_stats, pages_per_range, ...).
Or make it configurable when creating the index.

Another option would be to implement scalable bloom filters, i.e.
instead of creating one filter and stop using it when it gets too
full, create another (larger) one and use it for new additions.
But considering we can only see very limited number of distinct
values per range, that seems like an overkill, and it might make
certain operations (particularly "union" support procedure) more
complicated.

The patch also implements an optimization that we initially keep
the values "raw", and only switch to the actual bloom filter once
we use the same space. This somewhat reduces the space usage when
the column is low-cardinality (at least within a page range), so
that the bloom filter defaults would be too high.
---
 doc/src/sgml/brin.sgml                   | 229 ++++++++++
 src/backend/access/brin/Makefile         |   2 +-
 src/backend/access/brin/brin_bloom.c     | 755 +++++++++++++++++++++++++++++++
 src/include/catalog/pg_amop.h            |  59 +++
 src/include/catalog/pg_amproc.h          | 153 +++++++
 src/include/catalog/pg_opclass.h         |  25 +
 src/include/catalog/pg_opfamily.h        |  20 +
 src/include/catalog/pg_proc.h            |  10 +
 src/test/regress/expected/brin_bloom.out | 438 ++++++++++++++++++
 src/test/regress/expected/opr_sanity.out |   3 +-
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/serial_schedule         |   1 +
 src/test/regress/sql/brin_bloom.sql      | 391 ++++++++++++++++
 13 files changed, 2085 insertions(+), 3 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 23c0e05..434538e 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -118,6 +118,13 @@
    </thead>
    <tbody>
     <row>
+     <entry><literal>abstime_bloom_ops</literal></entry>
+     <entry><type>abstime</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>abstime_minmax_ops</literal></entry>
      <entry><type>abstime</type></entry>
      <entry>
@@ -129,6 +136,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>int8_bloom_ops</literal></entry>
+     <entry><type>bigint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int8_minmax_ops</literal></entry>
      <entry><type>bigint</type></entry>
      <entry>
@@ -180,6 +194,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>bytea_bloom_ops</literal></entry>
+     <entry><type>bytea</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>bytea_minmax_ops</literal></entry>
      <entry><type>bytea</type></entry>
      <entry>
@@ -191,6 +212,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>bpchar_bloom_ops</literal></entry>
+     <entry><type>character</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>bpchar_minmax_ops</literal></entry>
      <entry><type>character</type></entry>
      <entry>
@@ -202,6 +230,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>char_bloom_ops</literal></entry>
+     <entry><type>"char"</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>char_minmax_ops</literal></entry>
      <entry><type>"char"</type></entry>
      <entry>
@@ -213,6 +248,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>date_bloom_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>date_minmax_ops</literal></entry>
      <entry><type>date</type></entry>
      <entry>
@@ -224,6 +266,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>float8_bloom_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>float8_minmax_ops</literal></entry>
      <entry><type>double precision</type></entry>
      <entry>
@@ -235,6 +284,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>inet_bloom_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>inet_minmax_ops</literal></entry>
      <entry><type>inet</type></entry>
      <entry>
@@ -258,6 +314,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>int4_bloom_ops</literal></entry>
+     <entry><type>integer</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int4_minmax_ops</literal></entry>
      <entry><type>integer</type></entry>
      <entry>
@@ -269,6 +332,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>interval_bloom_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>interval_minmax_ops</literal></entry>
      <entry><type>interval</type></entry>
      <entry>
@@ -280,6 +350,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>macaddr_bloom_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>macaddr_minmax_ops</literal></entry>
      <entry><type>macaddr</type></entry>
      <entry>
@@ -291,6 +368,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>macaddr8_minmax_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
      <entry>
@@ -302,6 +386,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>name_bloom_ops</literal></entry>
+     <entry><type>name</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>name_minmax_ops</literal></entry>
      <entry><type>name</type></entry>
      <entry>
@@ -313,6 +404,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>numeric_bloom_ops</literal></entry>
+     <entry><type>numeric</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>numeric_minmax_ops</literal></entry>
      <entry><type>numeric</type></entry>
      <entry>
@@ -324,6 +422,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><type>pg_lsn</type></entry>
      <entry>
@@ -335,6 +440,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>oid_bloom_ops</literal></entry>
+     <entry><type>oid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>oid_minmax_ops</literal></entry>
      <entry><type>oid</type></entry>
      <entry>
@@ -366,6 +478,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>float4_bloom_ops</literal></entry>
+     <entry><type>real</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>float4_minmax_ops</literal></entry>
      <entry><type>real</type></entry>
      <entry>
@@ -377,6 +496,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>reltime_bloom_ops</literal></entry>
+     <entry><type>reltime</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>reltime_minmax_ops</literal></entry>
      <entry><type>reltime</type></entry>
      <entry>
@@ -388,6 +514,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>int2_bloom_ops</literal></entry>
+     <entry><type>smallint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int2_minmax_ops</literal></entry>
      <entry><type>smallint</type></entry>
      <entry>
@@ -399,6 +532,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>text_bloom_ops</literal></entry>
+     <entry><type>text</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>text_minmax_ops</literal></entry>
      <entry><type>text</type></entry>
      <entry>
@@ -421,6 +561,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timestamp_bloom_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timestamp_minmax_ops</literal></entry>
      <entry><type>timestamp without time zone</type></entry>
      <entry>
@@ -432,6 +579,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timestamptz_minmax_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
      <entry>
@@ -443,6 +597,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>time_bloom_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>time_minmax_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
      <entry>
@@ -454,6 +615,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timetz_bloom_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timetz_minmax_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
      <entry>
@@ -465,6 +633,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>uuid_bloom_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>uuid_minmax_ops</literal></entry>
      <entry><type>uuid</type></entry>
      <entry>
@@ -814,6 +989,60 @@ typedef struct BrinOpcInfo
  </para>
 
  <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
+ <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
     The minmax operator class requires a full set of operators to be
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 5aef925..a76d927 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = brin.o brin_pageops.o brin_revmap.o brin_tuple.o brin_xlog.o \
-       brin_minmax.o brin_inclusion.o brin_validate.o
+       brin_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000..461bfce
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,755 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		4	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+#define		BLOOM_PHASE_SORTED		1
+#define		BLOOM_PHASE_HASH		2
+
+/* how many hashes to accumulate before hashing */
+#define		BLOOM_MAX_UNSORTED		32
+#define		BLOOM_GROW_BYTES		32
+#define		BLOOM_NDISTINCT			1000	/* number of distinct values */
+#define		BLOOM_ERROR_RATE		0.05	/* 2% false positive rate */
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Maybe instead of explicitly limiting the number of unsorted values
+ * by BLOOM_MAX_UNSORTED, we should cap them by (filter size / 4B), i.e.
+ * allow up to the whole filter size.
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* global bloom filter parameters */
+	uint32	phase;		/* phase (initially SORTED, then HASH) */
+	uint32	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (optimal) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* fields used only in the EXACT phase */
+	uint32	nvalues;	/* number of hashes stored (sorted + extra) */
+	uint32	nsorted;	/* number of uint32 hashes in sorted part */
+
+	/* bitmap of the bloom filter */
+	char 	bitmap[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	/* https://en.wikipedia.org/wiki/Bloom_filter */
+	int m;	/* number of bits */
+	int k;	/* number of hash functions */
+
+	Assert((ndistinct > 0) && (ndistinct < 10000));	/* 10k is mostly arbitrary limit */
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	k = round(log(2.0) * m / ndistinct);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 */
+	len = Max(offsetof(BloomFilter, bitmap), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->phase = BLOOM_PHASE_SORTED;
+	filter->nhashes = k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * XXX Actually, we don't need to do repalloc - we just need to set the
+ * varlena header length!
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i,
+			nvalues;
+	uint32 *values;
+
+	/* never call compact on filters in HASH phase */
+	Assert(filter->phase == BLOOM_PHASE_SORTED);
+
+	/* no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) palloc(filter->nvalues * sizeof(uint32));
+
+	/* copy the data, then reset the bitmap */
+	memcpy(values, filter->bitmap, filter->nvalues * sizeof(uint32));
+	memset(filter->bitmap, 0, filter->nvalues * sizeof(uint32));
+
+	/* FIXME optimization: sort only the unsorted part, then merge */
+	pg_qsort(values, filter->nvalues, sizeof(uint32), cmp_uint32);
+
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if a new value, keep it */
+		if (values[i] != values[i-1])
+		{
+			values[nvalues] = values[i];
+			nvalues++;
+		}
+	}
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	memcpy(filter->bitmap, values, nvalues * sizeof(uint32));
+
+	pfree(values);
+}
+
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter);
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (filter->phase == BLOOM_PHASE_SORTED)
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter,bitmap) + (filter->nvalues+1) * sizeof(uint32);
+
+			if (len < need)
+			{
+				/*
+				 * We don't double the size here, as in the first place we care about
+				 * reducing storage requirements, and the doubling happens automatically
+				 * in memory contexts anyway.
+				 *
+				 * XXX Zero the newly allocated part. Maybe not really needed?
+				 */
+				filter = (BloomFilter *) repalloc(filter, len + BLOOM_GROW_BYTES);
+				memset((char *)filter + len, 0, BLOOM_GROW_BYTES);
+				SET_VARSIZE(filter, len + BLOOM_GROW_BYTES);
+			}
+
+			/* copy the data into the bitmap */
+			memcpy(&filter->bitmap[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(filter->phase == BLOOM_PHASE_HASH);
+
+    /* compute the hashes, used for the bloom filter */
+    big_h = ((uint32)DatumGetInt64(hash_uint32(value)));
+
+    h = big_h % filter->nbits;
+    d = big_h % (filter->nbits - 1); 
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->bitmap[byte] & (0x01 << bit)))
+		{
+			filter->bitmap[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+        h += d++;
+        if (h >= filter->nbits) h -= filter->nbits;
+        if (d == filter->nbits) d = 0; 
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, bitmap) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->phase = BLOOM_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->bitmap;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (filter->phase == BLOOM_PHASE_SORTED)
+	{
+		int i;
+		uint32 *values = (uint32 *)filter->bitmap;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+			if (value == values[i])
+				return true;
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(filter->phase == BLOOM_PHASE_HASH);
+
+    big_h = ((uint32)DatumGetInt64(hash_uint32(value)));
+
+    h = big_h % filter->nbits;
+    d = big_h % (filter->nbits - 1); 
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->bitmap[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+        h += d++;
+        if (h >= filter->nbits) h -= filter->nbits;
+        if (d == filter->nbits) d = 0; 
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 * 
+	 * bloom indexes only store a the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	/*
+	 * If the new value is null, we record that we saw it if it's the first
+	 * one; otherwise, there's nothing to do.
+	 */
+	if (isnull)
+	{
+		if (column->bv_hasnulls)
+			PG_RETURN_BOOL(false);
+
+		column->bv_hasnulls = true;
+		PG_RETURN_BOOL(true);
+	}
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(BLOOM_NDISTINCT, BLOOM_ERROR_RATE);
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+
+	/* Adjust "hasnulls" */
+	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+		col_a->bv_hasnulls = true;
+
+	/* If there are no values in B, there's nothing left to do */
+	if (col_b->bv_allnulls)
+		PG_RETURN_VOID();
+
+	/*
+	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
+	 * B into A, and we're done.  We cannot run the operators in this case,
+	 * because values in A might contain garbage.  Note we already established
+	 * that B contains values.
+	 */
+	if (col_a->bv_allnulls)
+	{
+		col_a->bv_allnulls = false;
+		col_a->bv_values[0] = datumCopy(col_b->bv_values[0], false, -1);
+		PG_RETURN_VOID();
+	}
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (filter_b->phase == BLOOM_PHASE_SORTED)
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->bitmap;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (filter_a->phase == BLOOM_PHASE_SORTED)
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->bitmap;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (filter_a->phase == BLOOM_PHASE_HASH)
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->bitmap[i] |= filter_b->bitmap[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
diff --git a/src/include/catalog/pg_amop.h b/src/include/catalog/pg_amop.h
index 03af581..84dc6e3 100644
--- a/src/include/catalog/pg_amop.h
+++ b/src/include/catalog/pg_amop.h
@@ -910,18 +910,24 @@ DATA(insert (	4064	 17   17 2 s	  1958	  3580 0 ));
 DATA(insert (	4064	 17   17 3 s	  1955	  3580 0 ));
 DATA(insert (	4064	 17   17 4 s	  1960	  3580 0 ));
 DATA(insert (	4064	 17   17 5 s	  1959	  3580 0 ));
+/* bloom bytea */
+DATA(insert (	5021	 17   17 1 s	  1955	  3580 0 ));
 /* minmax "char" */
 DATA(insert (	4062	 18   18 1 s	   631	  3580 0 ));
 DATA(insert (	4062	 18   18 2 s	   632	  3580 0 ));
 DATA(insert (	4062	 18   18 3 s		92	  3580 0 ));
 DATA(insert (	4062	 18   18 4 s	   634	  3580 0 ));
 DATA(insert (	4062	 18   18 5 s	   633	  3580 0 ));
+/* bloom "char" */
+DATA(insert (	5022	 18   18 1 s		92	  3580 0 ));
 /* minmax name */
 DATA(insert (	4065	 19   19 1 s	   660	  3580 0 ));
 DATA(insert (	4065	 19   19 2 s	   661	  3580 0 ));
 DATA(insert (	4065	 19   19 3 s		93	  3580 0 ));
 DATA(insert (	4065	 19   19 4 s	   663	  3580 0 ));
 DATA(insert (	4065	 19   19 5 s	   662	  3580 0 ));
+/* bloom name */
+DATA(insert (	5023	 19   19 1 s		93	  3580 0 ));
 /* minmax integer */
 DATA(insert (	4054	 20   20 1 s	   412	  3580 0 ));
 DATA(insert (	4054	 20   20 2 s	   414	  3580 0 ));
@@ -968,6 +974,16 @@ DATA(insert (	4054	 23   20 2 s		80	  3580 0 ));
 DATA(insert (	4054	 23   20 3 s		15	  3580 0 ));
 DATA(insert (	4054	 23   20 4 s		82	  3580 0 ));
 DATA(insert (	4054	 23   20 5 s		76	  3580 0 ));
+/* bloom integer */
+DATA(insert (	5024	 20   20 1 s	   410	  3580 0 ));
+DATA(insert (	5024	 20   21 1 s	  1868	  3580 0 ));
+DATA(insert (	5024	 20   23 1 s	   416	  3580 0 ));
+DATA(insert (	5024	 21   21 1 s		94	  3580 0 ));
+DATA(insert (	5024	 21   20 1 s	  1862	  3580 0 ));
+DATA(insert (	5024	 21   23 1 s	   532	  3580 0 ));
+DATA(insert (	5024	 23   23 1 s		96	  3580 0 ));
+DATA(insert (	5024	 23   21 1 s	   533	  3580 0 ));
+DATA(insert (	5024	 23   20 1 s		15	  3580 0 ));
 
 /* minmax text */
 DATA(insert (	4056	 25   25 1 s	   664	  3580 0 ));
@@ -975,12 +991,16 @@ DATA(insert (	4056	 25   25 2 s	   665	  3580 0 ));
 DATA(insert (	4056	 25   25 3 s		98	  3580 0 ));
 DATA(insert (	4056	 25   25 4 s	   667	  3580 0 ));
 DATA(insert (	4056	 25   25 5 s	   666	  3580 0 ));
+/* bloom text */
+DATA(insert (	5027	 25   25 1 s		98	  3580 0 ));
 /* minmax oid */
 DATA(insert (	4068	 26   26 1 s	   609	  3580 0 ));
 DATA(insert (	4068	 26   26 2 s	   611	  3580 0 ));
 DATA(insert (	4068	 26   26 3 s	   607	  3580 0 ));
 DATA(insert (	4068	 26   26 4 s	   612	  3580 0 ));
 DATA(insert (	4068	 26   26 5 s	   610	  3580 0 ));
+/* bloom oid */
+DATA(insert (	5029	 26   26 1 s	   607	  3580 0 ));
 /* minmax tid */
 DATA(insert (	4069	 27   27 1 s	  2799	  3580 0 ));
 DATA(insert (	4069	 27   27 2 s	  2801	  3580 0 ));
@@ -1008,6 +1028,11 @@ DATA(insert (	4070	701  701 2 s	   673	  3580 0 ));
 DATA(insert (	4070	701  701 3 s	   670	  3580 0 ));
 DATA(insert (	4070	701  701 4 s	   675	  3580 0 ));
 DATA(insert (	4070	701  701 5 s	   674	  3580 0 ));
+/* bloom float (float4, float8) */
+DATA(insert (	5030	700  700 1 s	   620	  3580 0 ));
+DATA(insert (	5030	700  701 1 s	  1120	  3580 0 ));
+DATA(insert (	5030	701  700 1 s	  1130	  3580 0 ));
+DATA(insert (	5030	701  701 1 s	   670	  3580 0 ));
 
 /* minmax abstime */
 DATA(insert (	4072	702  702 1 s	   562	  3580 0 ));
@@ -1015,24 +1040,32 @@ DATA(insert (	4072	702  702 2 s	   564	  3580 0 ));
 DATA(insert (	4072	702  702 3 s	   560	  3580 0 ));
 DATA(insert (	4072	702  702 4 s	   565	  3580 0 ));
 DATA(insert (	4072	702  702 5 s	   563	  3580 0 ));
+/* bloom abstime */
+DATA(insert (	5031	702  702 1 s	   560	  3580 0 ));
 /* minmax reltime */
 DATA(insert (	4073	703  703 1 s	   568	  3580 0 ));
 DATA(insert (	4073	703  703 2 s	   570	  3580 0 ));
 DATA(insert (	4073	703  703 3 s	   566	  3580 0 ));
 DATA(insert (	4073	703  703 4 s	   571	  3580 0 ));
 DATA(insert (	4073	703  703 5 s	   569	  3580 0 ));
+/* bloom reltime */
+DATA(insert (	5032	703  703 1 s	   566	  3580 0 ));
 /* minmax macaddr */
 DATA(insert (	4074	829  829 1 s	  1222	  3580 0 ));
 DATA(insert (	4074	829  829 2 s	  1223	  3580 0 ));
 DATA(insert (	4074	829  829 3 s	  1220	  3580 0 ));
 DATA(insert (	4074	829  829 4 s	  1225	  3580 0 ));
 DATA(insert (	4074	829  829 5 s	  1224	  3580 0 ));
+/* bloom macaddr */
+DATA(insert (	5033	829  829 1 s	  1220	  3580 0 ));
 /* minmax macaddr8 */
 DATA(insert (	4109	774  774 1 s	  3364	  3580 0 ));
 DATA(insert (	4109	774  774 2 s	  3365	  3580 0 ));
 DATA(insert (	4109	774  774 3 s	  3362	  3580 0 ));
 DATA(insert (	4109	774  774 4 s	  3367	  3580 0 ));
 DATA(insert (	4109	774  774 5 s	  3366	  3580 0 ));
+/* bloom macaddr8 */
+DATA(insert (	5034	774  774 1 s	  3362	  3580 0 ));
 /* minmax inet */
 DATA(insert (	4075	869  869 1 s	  1203	  3580 0 ));
 DATA(insert (	4075	869  869 2 s	  1204	  3580 0 ));
@@ -1046,18 +1079,24 @@ DATA(insert (	4102	869  869 8 s	   932	  3580 0 ));
 DATA(insert (	4102	869  869 18 s	  1201	  3580 0 ));
 DATA(insert (	4102	869  869 24 s	   933	  3580 0 ));
 DATA(insert (	4102	869  869 26 s	   931	  3580 0 ));
+/* bloom inet */
+DATA(insert (	5035	869  869 1 s	  1201	  3580 0 ));
 /* minmax character */
 DATA(insert (	4076   1042 1042 1 s	  1058	  3580 0 ));
 DATA(insert (	4076   1042 1042 2 s	  1059	  3580 0 ));
 DATA(insert (	4076   1042 1042 3 s	  1054	  3580 0 ));
 DATA(insert (	4076   1042 1042 4 s	  1061	  3580 0 ));
 DATA(insert (	4076   1042 1042 5 s	  1060	  3580 0 ));
+/* bloom character */
+DATA(insert (	5036   1042 1042 1 s	  1054	  3580 0 ));
 /* minmax time without time zone */
 DATA(insert (	4077   1083 1083 1 s	  1110	  3580 0 ));
 DATA(insert (	4077   1083 1083 2 s	  1111	  3580 0 ));
 DATA(insert (	4077   1083 1083 3 s	  1108	  3580 0 ));
 DATA(insert (	4077   1083 1083 4 s	  1113	  3580 0 ));
 DATA(insert (	4077   1083 1083 5 s	  1112	  3580 0 ));
+/* bloom time without time zone */
+DATA(insert (	5037   1083 1083 1 s	  1108	  3580 0 ));
 /* minmax datetime (date, timestamp, timestamptz) */
 DATA(insert (	4059   1114 1114 1 s	  2062	  3580 0 ));
 DATA(insert (	4059   1114 1114 2 s	  2063	  3580 0 ));
@@ -1104,6 +1143,16 @@ DATA(insert (	4059   1184 1184 2 s	  1323	  3580 0 ));
 DATA(insert (	4059   1184 1184 3 s	  1320	  3580 0 ));
 DATA(insert (	4059   1184 1184 4 s	  1325	  3580 0 ));
 DATA(insert (	4059   1184 1184 5 s	  1324	  3580 0 ));
+/* bloom datetime (date, timestamp, timestamptz) */
+DATA(insert (	5038   1114 1114 1 s	  2060	  3580 0 ));
+DATA(insert (	5038   1114 1082 1 s	  2373	  3580 0 ));
+DATA(insert (	5038   1114 1184 1 s	  2536	  3580 0 ));
+DATA(insert (	5038   1082 1082 1 s	  1093	  3580 0 ));
+DATA(insert (	5038   1082 1114 1 s	  2347	  3580 0 ));
+DATA(insert (	5038   1082 1184 1 s	  2360	  3580 0 ));
+DATA(insert (	5038   1184 1082 1 s	  2386	  3580 0 ));
+DATA(insert (	5038   1184 1114 1 s	  2542	  3580 0 ));
+DATA(insert (	5038   1184 1184 1 s	  1320	  3580 0 ));
 
 /* minmax interval */
 DATA(insert (	4078   1186 1186 1 s	  1332	  3580 0 ));
@@ -1111,12 +1160,16 @@ DATA(insert (	4078   1186 1186 2 s	  1333	  3580 0 ));
 DATA(insert (	4078   1186 1186 3 s	  1330	  3580 0 ));
 DATA(insert (	4078   1186 1186 4 s	  1335	  3580 0 ));
 DATA(insert (	4078   1186 1186 5 s	  1334	  3580 0 ));
+/* bloom interval */
+DATA(insert (	5041   1186 1186 1 s	  1330	  3580 0 ));
 /* minmax time with time zone */
 DATA(insert (	4058   1266 1266 1 s	  1552	  3580 0 ));
 DATA(insert (	4058   1266 1266 2 s	  1553	  3580 0 ));
 DATA(insert (	4058   1266 1266 3 s	  1550	  3580 0 ));
 DATA(insert (	4058   1266 1266 4 s	  1555	  3580 0 ));
 DATA(insert (	4058   1266 1266 5 s	  1554	  3580 0 ));
+/* bloom time with time zone */
+DATA(insert (	5042   1266 1266 1 s	  1550	  3580 0 ));
 /* minmax bit */
 DATA(insert (	4079   1560 1560 1 s	  1786	  3580 0 ));
 DATA(insert (	4079   1560 1560 2 s	  1788	  3580 0 ));
@@ -1135,12 +1188,16 @@ DATA(insert (	4055   1700 1700 2 s	  1755	  3580 0 ));
 DATA(insert (	4055   1700 1700 3 s	  1752	  3580 0 ));
 DATA(insert (	4055   1700 1700 4 s	  1757	  3580 0 ));
 DATA(insert (	4055   1700 1700 5 s	  1756	  3580 0 ));
+/* bloom numeric */
+DATA(insert (	5045   1700 1700 1 s	  1752	  3580 0 ));
 /* minmax uuid */
 DATA(insert (	4081   2950 2950 1 s	  2974	  3580 0 ));
 DATA(insert (	4081   2950 2950 2 s	  2976	  3580 0 ));
 DATA(insert (	4081   2950 2950 3 s	  2972	  3580 0 ));
 DATA(insert (	4081   2950 2950 4 s	  2977	  3580 0 ));
 DATA(insert (	4081   2950 2950 5 s	  2975	  3580 0 ));
+/* bloom uuid */
+DATA(insert (	5046   2950 2950 1 s	  2972	  3580 0 ));
 /* inclusion range types */
 DATA(insert (	4103   3831 3831  1 s	  3893	  3580 0 ));
 DATA(insert (	4103   3831 3831  2 s	  3895	  3580 0 ));
@@ -1162,6 +1219,8 @@ DATA(insert (	4082   3220 3220 2 s	  3226	  3580 0 ));
 DATA(insert (	4082   3220 3220 3 s	  3222	  3580 0 ));
 DATA(insert (	4082   3220 3220 4 s	  3227	  3580 0 ));
 DATA(insert (	4082   3220 3220 5 s	  3225	  3580 0 ));
+/* bloom pg_lsn */
+DATA(insert (	5047   3220 3220 1 s	  3222	  3580 0 ));
 /* inclusion box */
 DATA(insert (	4104	603  603  1 s	   493	  3580 0 ));
 DATA(insert (	4104	603  603  2 s	   494	  3580 0 ));
diff --git a/src/include/catalog/pg_amproc.h b/src/include/catalog/pg_amproc.h
index f545a058..4871836 100644
--- a/src/include/catalog/pg_amproc.h
+++ b/src/include/catalog/pg_amproc.h
@@ -347,16 +347,34 @@ DATA(insert (	4064	17	  17  1  3383 ));
 DATA(insert (	4064	17	  17  2  3384 ));
 DATA(insert (	4064	17	  17  3  3385 ));
 DATA(insert (	4064	17	  17  4  3386 ));
+/* bloom bytea */
+DATA(insert (	5021	17	  17  1  5017 ));
+DATA(insert (	5021	17	  17  2  5018 ));
+DATA(insert (	5021	17	  17  3  5019 ));
+DATA(insert (	5021	17	  17  4  5020 ));
+DATA(insert (	5021	17	  17 11   456 ));
 /* minmax "char" */
 DATA(insert (	4062	18	  18  1  3383 ));
 DATA(insert (	4062	18	  18  2  3384 ));
 DATA(insert (	4062	18	  18  3  3385 ));
 DATA(insert (	4062	18	  18  4  3386 ));
+/* bloom "char" */
+DATA(insert (	5022	18	  18  1  5017 ));
+DATA(insert (	5022	18	  18  2  5018 ));
+DATA(insert (	5022	18	  18  3  5019 ));
+DATA(insert (	5022	18	  18  4  5020 ));
+DATA(insert (	5022	18	  18 11   454 ));
 /* minmax name */
 DATA(insert (	4065	19	  19  1  3383 ));
 DATA(insert (	4065	19	  19  2  3384 ));
 DATA(insert (	4065	19	  19  3  3385 ));
 DATA(insert (	4065	19	  19  4  3386 ));
+/* bloom name */
+DATA(insert (	5023	19	  19  1  5017 ));
+DATA(insert (	5023	19	  19  2  5018 ));
+DATA(insert (	5023	19	  19  3  5019 ));
+DATA(insert (	5023	19	  19  4  5020 ));
+DATA(insert (	5023	19	  19 11   455 ));
 /* minmax integer: int2, int4, int8 */
 DATA(insert (	4054	20	  20  1  3383 ));
 DATA(insert (	4054	20	  20  2  3384 ));
@@ -397,16 +415,47 @@ DATA(insert (	4054	23	  21  2  3384 ));
 DATA(insert (	4054	23	  21  3  3385 ));
 DATA(insert (	4054	23	  21  4  3386 ));
 
+/* bloom integer: int2, int4, int8 */
+DATA(insert (	5024	20	  20  1  5017 ));
+DATA(insert (	5024	20	  20  2  5018 ));
+DATA(insert (	5024	20	  20  3  5019 ));
+DATA(insert (	5024	20	  20  4  5020 ));
+DATA(insert (	5024	20	  20 11   949 ));
+
+DATA(insert (	5024	21	  21  1  5017 ));
+DATA(insert (	5024	21	  21  2  5018 ));
+DATA(insert (	5024	21	  21  3  5019 ));
+DATA(insert (	5024	21	  21  4  5020 ));
+DATA(insert (	5024	21	  21 11   449 ));
+
+DATA(insert (	5024	23	  23  1  5017 ));
+DATA(insert (	5024	23	  23  2  5018 ));
+DATA(insert (	5024	23	  23  3  5019 ));
+DATA(insert (	5024	23	  23  4  5020 ));
+DATA(insert (	5024	23	  23 11   450 ));
+
 /* minmax text */
 DATA(insert (	4056	25	  25  1  3383 ));
 DATA(insert (	4056	25	  25  2  3384 ));
 DATA(insert (	4056	25	  25  3  3385 ));
 DATA(insert (	4056	25	  25  4  3386 ));
+/* bloom text */
+DATA(insert (	5027	25	  25  1  5017 ));
+DATA(insert (	5027	25	  25  2  5018 ));
+DATA(insert (	5027	25	  25  3  5019 ));
+DATA(insert (	5027	25	  25  4  5020 ));
+DATA(insert (	5027	25	  25 11   400 ));
 /* minmax oid */
 DATA(insert (	4068	26	  26  1  3383 ));
 DATA(insert (	4068	26	  26  2  3384 ));
 DATA(insert (	4068	26	  26  3  3385 ));
 DATA(insert (	4068	26	  26  4  3386 ));
+/* bloom oid */
+DATA(insert (	5029	26	  26  1  5017 ));
+DATA(insert (	5029	26	  26  2  5018 ));
+DATA(insert (	5029	26	  26  3  5019 ));
+DATA(insert (	5029	26	  26  4  5020 ));
+DATA(insert (	5029	26	  26 11   453 ));
 /* minmax tid */
 DATA(insert (	4069	27	  27  1  3383 ));
 DATA(insert (	4069	27	  27  2  3384 ));
@@ -433,26 +482,63 @@ DATA(insert (	4070   701	 700  2  3384 ));
 DATA(insert (	4070   701	 700  3  3385 ));
 DATA(insert (	4070   701	 700  4  3386 ));
 
+/* bloom float */
+DATA(insert (	5030   700	 700  1  5017 ));
+DATA(insert (	5030   700	 700  2  5018 ));
+DATA(insert (	5030   700	 700  3  5019 ));
+DATA(insert (	5030   700	 700  4  5020 ));
+DATA(insert (	5030   700	 700 11   451 ));
+
+DATA(insert (	5030   701	 701  1  5017 ));
+DATA(insert (	5030   701	 701  2  5018 ));
+DATA(insert (	5030   701	 701  3  5019 ));
+DATA(insert (	5030   701	 701  4  5020 ));
+DATA(insert (	5030   701	 701 11   452 ));
+
 /* minmax abstime */
 DATA(insert (	4072   702	 702  1  3383 ));
 DATA(insert (	4072   702	 702  2  3384 ));
 DATA(insert (	4072   702	 702  3  3385 ));
 DATA(insert (	4072   702	 702  4  3386 ));
+/* bloom abstime */
+DATA(insert (	5031   702	 702  1  5017 ));
+DATA(insert (	5031   702	 702  2  5018 ));
+DATA(insert (	5031   702	 702  3  5019 ));
+DATA(insert (	5031   702	 702  4  5020 ));
+DATA(insert (	5031   702	 702 11   450 ));
 /* minmax reltime */
 DATA(insert (	4073   703	 703  1  3383 ));
 DATA(insert (	4073   703	 703  2  3384 ));
 DATA(insert (	4073   703	 703  3  3385 ));
 DATA(insert (	4073   703	 703  4  3386 ));
+/* bloom reltime */
+DATA(insert (	5032   703	 703  1  5017 ));
+DATA(insert (	5032   703	 703  2  5018 ));
+DATA(insert (	5032   703	 703  3  5019 ));
+DATA(insert (	5032   703	 703  4  5020 ));
+DATA(insert (	5032   703	 703 11   450 ));
 /* minmax macaddr */
 DATA(insert (	4074   829	 829  1  3383 ));
 DATA(insert (	4074   829	 829  2  3384 ));
 DATA(insert (	4074   829	 829  3  3385 ));
 DATA(insert (	4074   829	 829  4  3386 ));
+/* bloom macaddr */
+DATA(insert (	5033   829	 829  1  5017 ));
+DATA(insert (	5033   829	 829  2  5018 ));
+DATA(insert (	5033   829	 829  3  5019 ));
+DATA(insert (	5033   829	 829  4  5020 ));
+DATA(insert (	5033   829	 829 11   399 ));
 /* minmax macaddr8 */
 DATA(insert (	4109   774	 774  1  3383 ));
 DATA(insert (	4109   774	 774  2  3384 ));
 DATA(insert (	4109   774	 774  3  3385 ));
 DATA(insert (	4109   774	 774  4  3386 ));
+/* bloom macaddr8 */
+DATA(insert (	5034   774	 774  1  5017 ));
+DATA(insert (	5034   774	 774  2  5018 ));
+DATA(insert (	5034   774	 774  3  5019 ));
+DATA(insert (	5034   774	 774  4  5020 ));
+DATA(insert (	5034   774	 774 11   328 ));
 /* minmax inet */
 DATA(insert (	4075   869	 869  1  3383 ));
 DATA(insert (	4075   869	 869  2  3384 ));
@@ -466,16 +552,34 @@ DATA(insert (	4102   869	 869  4  4108 ));
 DATA(insert (	4102   869	 869 11  4063 ));
 DATA(insert (	4102   869	 869 12  4071 ));
 DATA(insert (	4102   869	 869 13   930 ));
+/* bloom inet */
+DATA(insert (	5035   869	 869  1  5017 ));
+DATA(insert (	5035   869	 869  2  5018 ));
+DATA(insert (	5035   869	 869  3  5019 ));
+DATA(insert (	5035   869	 869  4  5020 ));
+DATA(insert (	5035   869	 869 11   422 ));
 /* minmax character */
 DATA(insert (	4076  1042	1042  1  3383 ));
 DATA(insert (	4076  1042	1042  2  3384 ));
 DATA(insert (	4076  1042	1042  3  3385 ));
 DATA(insert (	4076  1042	1042  4  3386 ));
+/* bloom character */
+DATA(insert (	5036  1042	1042  1  5017 ));
+DATA(insert (	5036  1042	1042  2  5018 ));
+DATA(insert (	5036  1042	1042  3  5019 ));
+DATA(insert (	5036  1042	1042  4  5020 ));
+DATA(insert (	5036  1042	1042 11   454 ));
 /* minmax time without time zone */
 DATA(insert (	4077  1083	1083  1  3383 ));
 DATA(insert (	4077  1083	1083  2  3384 ));
 DATA(insert (	4077  1083	1083  3  3385 ));
 DATA(insert (	4077  1083	1083  4  3386 ));
+/* bloom time without time zone */
+DATA(insert (	5037  1083	1083  1  5017 ));
+DATA(insert (	5037  1083	1083  2  5018 ));
+DATA(insert (	5037  1083	1083  3  5019 ));
+DATA(insert (	5037  1083	1083  4  5020 ));
+DATA(insert (	5037  1083	1083 11  1688 ));
 /* minmax datetime (date, timestamp, timestamptz) */
 DATA(insert (	4059  1114	1114  1  3383 ));
 DATA(insert (	4059  1114	1114  2  3384 ));
@@ -516,16 +620,47 @@ DATA(insert (	4059  1082	1184  2  3384 ));
 DATA(insert (	4059  1082	1184  3  3385 ));
 DATA(insert (	4059  1082	1184  4  3386 ));
 
+/* bloom datetime (date, timestamp, timestamptz) */
+DATA(insert (	5038  1114	1114  1  5017 ));
+DATA(insert (	5038  1114	1114  2  5018 ));
+DATA(insert (	5038  1114	1114  3  5019 ));
+DATA(insert (	5038  1114	1114  4  5020 ));
+DATA(insert (	5038  1114	1114 11  2039 ));
+
+DATA(insert (	5038  1184	1184  1  5017 ));
+DATA(insert (	5038  1184	1184  2  5018 ));
+DATA(insert (	5038  1184	1184  3  5019 ));
+DATA(insert (	5038  1184	1184  4  5020 ));
+DATA(insert (	5038  1184	1184 11  2039 ));
+
+DATA(insert (	5038  1082	1082  1  5017 ));
+DATA(insert (	5038  1082	1082  2  5018 ));
+DATA(insert (	5038  1082	1082  3  5019 ));
+DATA(insert (	5038  1082	1082  4  5020 ));
+DATA(insert (	5038  1082	1082 11  450 ));
+
 /* minmax interval */
 DATA(insert (	4078  1186	1186  1  3383 ));
 DATA(insert (	4078  1186	1186  2  3384 ));
 DATA(insert (	4078  1186	1186  3  3385 ));
 DATA(insert (	4078  1186	1186  4  3386 ));
+/* bloom interval */
+DATA(insert (	5041  1186	1186  1  5017 ));
+DATA(insert (	5041  1186	1186  2  5018 ));
+DATA(insert (	5041  1186	1186  3  5019 ));
+DATA(insert (	5041  1186	1186  4  5020 ));
+DATA(insert (	5041  1186	1186 11  1697 ));
 /* minmax time with time zone */
 DATA(insert (	4058  1266	1266  1  3383 ));
 DATA(insert (	4058  1266	1266  2  3384 ));
 DATA(insert (	4058  1266	1266  3  3385 ));
 DATA(insert (	4058  1266	1266  4  3386 ));
+/* bloom time with time zone */
+DATA(insert (	5042  1266	1266  1  5017 ));
+DATA(insert (	5042  1266	1266  2  5018 ));
+DATA(insert (	5042  1266	1266  3  5019 ));
+DATA(insert (	5042  1266	1266  4  5020 ));
+DATA(insert (	5042  1266	1266 11  1696 ));
 /* minmax bit */
 DATA(insert (	4079  1560	1560  1  3383 ));
 DATA(insert (	4079  1560	1560  2  3384 ));
@@ -541,11 +676,23 @@ DATA(insert (	4055  1700	1700  1  3383 ));
 DATA(insert (	4055  1700	1700  2  3384 ));
 DATA(insert (	4055  1700	1700  3  3385 ));
 DATA(insert (	4055  1700	1700  4  3386 ));
+/* bloom numeric */
+DATA(insert (	5045  1700	1700  1  5017 ));
+DATA(insert (	5045  1700	1700  2  5018 ));
+DATA(insert (	5045  1700	1700  3  5019 ));
+DATA(insert (	5045  1700	1700  4  5020 ));
+DATA(insert (	5045  1700	1700 11   432 ));
 /* minmax uuid */
 DATA(insert (	4081  2950	2950  1  3383 ));
 DATA(insert (	4081  2950	2950  2  3384 ));
 DATA(insert (	4081  2950	2950  3  3385 ));
 DATA(insert (	4081  2950	2950  4  3386 ));
+/* bloom uuid */
+DATA(insert (	5046  2950	2950  1  5017 ));
+DATA(insert (	5046  2950	2950  2  5018 ));
+DATA(insert (	5046  2950	2950  3  5019 ));
+DATA(insert (	5046  2950	2950  4  5020 ));
+DATA(insert (	5046  2950	2950 11  2963 ));
 /* inclusion range types */
 DATA(insert (	4103  3831	3831  1  4105 ));
 DATA(insert (	4103  3831	3831  2  4106 ));
@@ -559,6 +706,12 @@ DATA(insert (	4082  3220	3220  1  3383 ));
 DATA(insert (	4082  3220	3220  2  3384 ));
 DATA(insert (	4082  3220	3220  3  3385 ));
 DATA(insert (	4082  3220	3220  4  3386 ));
+/* bloom pg_lsn */
+DATA(insert (	5047  3220	3220  1  5017 ));
+DATA(insert (	5047  3220	3220  2  5018 ));
+DATA(insert (	5047  3220	3220  3  5019 ));
+DATA(insert (	5047  3220	3220  4  5020 ));
+DATA(insert (	5047  3220	3220 11  3252 ));
 /* inclusion box */
 DATA(insert (	4104   603	 603  1  4105 ));
 DATA(insert (	4104   603	 603  2  4106 ));
diff --git a/src/include/catalog/pg_opclass.h b/src/include/catalog/pg_opclass.h
index e11d52e..7dbd4ff 100644
--- a/src/include/catalog/pg_opclass.h
+++ b/src/include/catalog/pg_opclass.h
@@ -214,36 +214,61 @@ DATA(insert (	2742	jsonb_path_ops		PGNSP PGUID 4037  3802 f 23 ));
 /* BRIN operator classes */
 /* no brin opclass for bool */
 DATA(insert (	3580	bytea_minmax_ops		PGNSP PGUID 4064	17 t 17 ));
+DATA(insert (	3580	bytea_bloom_ops			PGNSP PGUID 5021	17 f 17 ));
 DATA(insert (	3580	char_minmax_ops			PGNSP PGUID 4062	18 t 18 ));
+DATA(insert (	3580	char_bloom_ops			PGNSP PGUID 5022	18 f 18 ));
 DATA(insert (	3580	name_minmax_ops			PGNSP PGUID 4065	19 t 19 ));
+DATA(insert (	3580	name_bloom_ops			PGNSP PGUID 5023	19 f 19 ));
 DATA(insert (	3580	int8_minmax_ops			PGNSP PGUID 4054	20 t 20 ));
+DATA(insert (	3580	int8_bloom_ops			PGNSP PGUID 5024	20 f 20 ));
 DATA(insert (	3580	int2_minmax_ops			PGNSP PGUID 4054	21 t 21 ));
+DATA(insert (	3580	int2_bloom_ops			PGNSP PGUID 5024	21 f 21 ));
 DATA(insert (	3580	int4_minmax_ops			PGNSP PGUID 4054	23 t 23 ));
+DATA(insert (	3580	int4_bloom_ops			PGNSP PGUID 5024	23 f 23 ));
 DATA(insert (	3580	text_minmax_ops			PGNSP PGUID 4056	25 t 25 ));
+DATA(insert (	3580	text_bloom_ops			PGNSP PGUID 5027	25 f 25 ));
 DATA(insert (	3580	oid_minmax_ops			PGNSP PGUID 4068	26 t 26 ));
+DATA(insert (	3580	oid_bloom_ops			PGNSP PGUID 5029	26 f 26 ));
 DATA(insert (	3580	tid_minmax_ops			PGNSP PGUID 4069	27 t 27 ));
 DATA(insert (	3580	float4_minmax_ops		PGNSP PGUID 4070   700 t 700 ));
+DATA(insert (	3580	float4_bloom_ops		PGNSP PGUID 5030   700 f 700 ));
 DATA(insert (	3580	float8_minmax_ops		PGNSP PGUID 4070   701 t 701 ));
+DATA(insert (	3580	float8_bloom_ops		PGNSP PGUID 5030   701 f 701 ));
 DATA(insert (	3580	abstime_minmax_ops		PGNSP PGUID 4072   702 t 702 ));
+DATA(insert (	3580	abstime_bloom_ops		PGNSP PGUID 5031   702 f 702 ));
 DATA(insert (	3580	reltime_minmax_ops		PGNSP PGUID 4073   703 t 703 ));
+DATA(insert (	3580	reltime_bloom_ops		PGNSP PGUID 5032   703 f 703 ));
 DATA(insert (	3580	macaddr_minmax_ops		PGNSP PGUID 4074   829 t 829 ));
+DATA(insert (	3580	macaddr_bloom_ops		PGNSP PGUID 5033   829 f 829 ));
 DATA(insert (	3580	macaddr8_minmax_ops		PGNSP PGUID 4109   774 t 774 ));
+DATA(insert (	3580	macaddr8_bloom_ops		PGNSP PGUID 5034   774 f 774 ));
 DATA(insert (	3580	inet_minmax_ops			PGNSP PGUID 4075   869 f 869 ));
 DATA(insert (	3580	inet_inclusion_ops		PGNSP PGUID 4102   869 t 869 ));
+DATA(insert (	3580	inet_bloom_ops			PGNSP PGUID 5035   869 f 869 ));
 DATA(insert (	3580	bpchar_minmax_ops		PGNSP PGUID 4076  1042 t 1042 ));
+DATA(insert (	3580	bpchar_bloom_ops		PGNSP PGUID 5036  1042 f 1042 ));
 DATA(insert (	3580	time_minmax_ops			PGNSP PGUID 4077  1083 t 1083 ));
+DATA(insert (	3580	time_bloom_ops			PGNSP PGUID 5037  1083 f 1083 ));
 DATA(insert (	3580	date_minmax_ops			PGNSP PGUID 4059  1082 t 1082 ));
+DATA(insert (	3580	date_bloom_ops			PGNSP PGUID 5038  1082 f 1082 ));
 DATA(insert (	3580	timestamp_minmax_ops	PGNSP PGUID 4059  1114 t 1114 ));
+DATA(insert (	3580	timestamp_bloom_ops		PGNSP PGUID 5038  1114 f 1114 ));
 DATA(insert (	3580	timestamptz_minmax_ops	PGNSP PGUID 4059  1184 t 1184 ));
+DATA(insert (	3580	timestamptz_bloom_ops	PGNSP PGUID 5038  1184 f 1184 ));
 DATA(insert (	3580	interval_minmax_ops		PGNSP PGUID 4078  1186 t 1186 ));
+DATA(insert (	3580	interval_bloom_ops		PGNSP PGUID 5041  1186 f 1186 ));
 DATA(insert (	3580	timetz_minmax_ops		PGNSP PGUID 4058  1266 t 1266 ));
+DATA(insert (	3580	timetz_bloom_ops		PGNSP PGUID 5042  1266 f 1266 ));
 DATA(insert (	3580	bit_minmax_ops			PGNSP PGUID 4079  1560 t 1560 ));
 DATA(insert (	3580	varbit_minmax_ops		PGNSP PGUID 4080  1562 t 1562 ));
 DATA(insert (	3580	numeric_minmax_ops		PGNSP PGUID 4055  1700 t 1700 ));
+DATA(insert (	3580	numeric_bloom_ops		PGNSP PGUID 5045  1700 f 1700 ));
 /* no brin opclass for record, anyarray */
 DATA(insert (	3580	uuid_minmax_ops			PGNSP PGUID 4081  2950 t 2950 ));
+DATA(insert (	3580	uuid_bloom_ops			PGNSP PGUID 5046  2950 f 2950 ));
 DATA(insert (	3580	range_inclusion_ops		PGNSP PGUID 4103  3831 t 3831 ));
 DATA(insert (	3580	pg_lsn_minmax_ops		PGNSP PGUID 4082  3220 t 3220 ));
+DATA(insert (	3580	pg_lsn_bloom_ops		PGNSP PGUID 5047  3220 f 3220 ));
 /* no brin opclass for enum, tsvector, tsquery, jsonb */
 DATA(insert (	3580	box_inclusion_ops		PGNSP PGUID 4104   603 t 603 ));
 /* no brin opclass for the geometric types except box */
diff --git a/src/include/catalog/pg_opfamily.h b/src/include/catalog/pg_opfamily.h
index b544474..bc22ce3 100644
--- a/src/include/catalog/pg_opfamily.h
+++ b/src/include/catalog/pg_opfamily.h
@@ -160,30 +160,50 @@ DATA(insert OID = 4036 (	2742	jsonb_ops		PGNSP PGUID ));
 DATA(insert OID = 4037 (	2742	jsonb_path_ops	PGNSP PGUID ));
 
 DATA(insert OID = 4054 (	3580	integer_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5024 (	3580	integer_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4055 (	3580	numeric_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5045 (	3580	numeric_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4056 (	3580	text_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5027 (	3580	text_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4058 (	3580	timetz_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5042 (	3580	timetz_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4059 (	3580	datetime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5038 (	3580	datetime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4062 (	3580	char_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5022 (	3580	char_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4064 (	3580	bytea_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5021 (	3580	bytea_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4065 (	3580	name_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5023 (	3580	name_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4068 (	3580	oid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5029 (	3580	oid_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4069 (	3580	tid_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 4070 (	3580	float_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5030 (	3580	float_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4072 (	3580	abstime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5031 (	3580	abstime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4073 (	3580	reltime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5032 (	3580	reltime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4074 (	3580	macaddr_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5033 (	3580	macaddr_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4109 (	3580	macaddr8_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5034 (	3580	macaddr8_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4075 (	3580	network_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 4102 (	3580	network_inclusion_ops	PGNSP PGUID ));
+DATA(insert OID = 5035 (	3580	network_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4076 (	3580	bpchar_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5036 (	3580	bpchar_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4077 (	3580	time_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5037 (	3580	time_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4078 (	3580	interval_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5041 (	3580	interval_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4079 (	3580	bit_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 4080 (	3580	varbit_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 4081 (	3580	uuid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5046 (	3580	uuid_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4103 (	3580	range_inclusion_ops		PGNSP PGUID ));
 DATA(insert OID = 4082 (	3580	pg_lsn_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5047 (	3580	pg_lsn_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4104 (	3580	box_inclusion_ops		PGNSP PGUID ));
 DATA(insert OID = 5000 (	4000	box_ops		PGNSP PGUID ));
 DATA(insert OID = 5008 (	4000	poly_ops				PGNSP PGUID ));
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index f01648c..7154e5e 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -4374,6 +4374,16 @@ DESCR("BRIN inclusion support");
 DATA(insert OID = 4108 ( brin_inclusion_union	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_inclusion_union _null_ _null_ _null_ ));
 DESCR("BRIN inclusion support");
 
+/* BRIN bloom */
+DATA(insert OID = 5017 ( brin_bloom_opcinfo		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 1 0 2281 "2281" _null_ _null_ _null_ _null_ _null_ brin_bloom_opcinfo _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+DATA(insert OID = 5018 ( brin_bloom_add_value	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 4 0 16 "2281 2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_bloom_add_value _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+DATA(insert OID = 5019 ( brin_bloom_consistent	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_bloom_consistent _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+DATA(insert OID = 5020 ( brin_bloom_union		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_bloom_union _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+
 /* userlock replacements */
 DATA(insert OID = 2880 (  pg_advisory_lock				PGNSP PGUID 12 1 0 0 0 f f f f t f v u 1 0 2278 "20" _null_ _null_ _null_ _null_ _null_ pg_advisory_lock_int8 _null_ _null_ _null_ ));
 DESCR("obtain exclusive advisory lock");
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000..ec6e27a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,438 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 684f7f2..91b2ecc 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -1819,6 +1819,7 @@ ORDER BY 1, 2, 3;
        2742 |           11 | ?&
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -1880,7 +1881,7 @@ ORDER BY 1, 2, 3;
        4000 |           25 | <<=
        4000 |           26 | >>
        4000 |           27 | >>=
-(121 rows)
+(122 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ad9434f..149999d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -84,7 +84,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password
+test: brin brin_bloom gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 27cd498..06c3488 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -106,6 +106,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000..8b2c005
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,391 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.9.5

0004-BRIN-multi-range-minmax-indexes.patchtext/x-patch; name=0004-BRIN-multi-range-minmax-indexes.patchDownload
From 871bab2388b79b5d4c3de2bb008a762ed2246986 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 9 Jan 2018 01:01:26 +0100
Subject: [PATCH 4/4] BRIN multi-range minmax indexes

The built-in minmax opclass works well for column correlated with
physical location in the table (e.g. timestamp in append-only
tables). But when the data is not perfectly correlated (possibly
due to later DELETE/UPDATE/INSERT commands), the minmax ranges
get very wide and ineffective to eliminate the page ranges.

This opclass deals with that by replacing the one [min,max] range
with multiple smaller ones. This allows us to track gaps, and deal
with outlier values.

Note: Currently, this only works with float8-based data types.
Supporting additional data types is not a big issue, but will
require extending the opclass with "subtract" operator (used to
compute distance between values when merging ranges).
---
 doc/src/sgml/brin.sgml                      |  234 ++-
 src/backend/access/brin/Makefile            |    3 +-
 src/backend/access/brin/brin_minmax_multi.c | 2200 +++++++++++++++++++++++++++
 src/include/catalog/pg_amop.h               |  191 +++
 src/include/catalog/pg_amproc.h             |  201 +++
 src/include/catalog/pg_opclass.h            |   21 +
 src/include/catalog/pg_opfamily.h           |   16 +
 src/include/catalog/pg_proc.h               |   44 +
 src/test/regress/expected/brin_multi.out    |  408 +++++
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/sql/brin_multi.sql         |  361 +++++
 11 files changed, 3672 insertions(+), 9 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 434538e..184c886 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -136,6 +136,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>abstime_minmax_multi_ops</literal></entry>
+     <entry><type>abstime</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int8_bloom_ops</literal></entry>
      <entry><type>bigint</type></entry>
      <entry>
@@ -266,6 +277,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>date_minmax_multi_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>float8_bloom_ops</literal></entry>
      <entry><type>double precision</type></entry>
      <entry>
@@ -284,6 +306,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>inet_bloom_ops</literal></entry>
      <entry><type>inet</type></entry>
      <entry>
@@ -302,6 +335,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>network_inclusion_ops</literal></entry>
      <entry><type>inet</type></entry>
      <entry>
@@ -350,6 +394,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>macaddr_bloom_ops</literal></entry>
      <entry><type>macaddr</type></entry>
      <entry>
@@ -368,6 +423,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>macaddr8_bloom_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
      <entry>
@@ -386,6 +452,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>name_bloom_ops</literal></entry>
      <entry><type>name</type></entry>
      <entry>
@@ -440,6 +517,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>oid_bloom_ops</literal></entry>
      <entry><type>oid</type></entry>
      <entry>
@@ -514,6 +602,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>reltime_minmax_multi_ops</literal></entry>
+     <entry><type>reltime</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int2_bloom_ops</literal></entry>
      <entry><type>smallint</type></entry>
      <entry>
@@ -579,6 +678,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timestamptz_bloom_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
      <entry>
@@ -597,6 +707,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>time_bloom_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
      <entry>
@@ -615,6 +736,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>time_minmax_multi_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timetz_bloom_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
      <entry>
@@ -633,6 +765,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>uuid_bloom_ops</literal></entry>
      <entry><type>uuid</type></entry>
      <entry>
@@ -650,6 +793,17 @@
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
    </tbody>
   </tgroup>
  </table>
@@ -744,13 +898,13 @@ typedef struct BrinOpcInfo
    </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support procedures' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions using
+  them are shipped for in-core data types as appropriate.  Additional operator
+  classes can be defined by the user for other data types using equivalent
+  definitions, without having to write any source code; appropriate catalog
+  entries being declared is enough.  Note that assumptions about the semantics
+  of operator strategies are embedded in the support procedures' source code.
  </para>
 
  <para>
@@ -820,6 +974,72 @@ typedef struct BrinOpcInfo
  </table>
 
  <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
   To write an operator class for a complex data type which has values
   included within another type, it's possible to use the inclusion support
   procedures alongside the corresponding operators, as shown
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index a76d927..c87c796 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -13,6 +13,7 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = brin.o brin_pageops.o brin_revmap.o brin_tuple.o brin_xlog.o \
-       brin_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o
+       brin_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o \
+       brin_minmax_multi.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000..cdb2857
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2200 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see MINMAX_MAX_VALUES). Collapsed intervals (with equal
+ * minimum and maximum value) are stored as a single value, while regular
+ * intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of MINMAX_MAX_VALUES), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/nabstime.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Maximum number of values (individual points or range boundaries) to
+ * keep in the summary values.
+ */
+#define		MINMAX_MAX_VALUES	64
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than MINMAX_MAX_VALUES values - if needed we perform
+ * compaction by merging some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for MINMAX_MAX_VALUES elements. This should be fine as long as
+ * the MINMAX_MAX_VALUES is reasonably small (64 seems fine).
+ */
+typedef struct Ranges
+{
+	/* (2*nranges + nvalues) <= MINMAX_MAX_VALUES */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* (2*nranges + nvalues) <= MINMAX_MAX_VALUES */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+static Ranges *range_deserialize(SerializedRanges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(void)
+{
+	Size				len;
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += MINMAX_MAX_VALUES * sizeof(Datum); /* Datum values */
+
+	return (Ranges *) palloc0(len);
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= MINMAX_MAX_VALUES);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (attr->attlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (attr->attlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(attr->attlen > 0);
+		len += nvalues * attr->attlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized,
+				AttrNumber attno, Form_pg_attribute attr)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+
+	Ranges *range = minmax_multi_init();
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= MINMAX_MAX_VALUES);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTLessStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ * 
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (MINMAX_MAX_VALUES).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count_values(cranges, ncranges) <= MINMAX_MAX_VALUES * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= MINMAX_MAX_VALUES);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit MINMAX_MAX_VALUES, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when MINMAX_MAX_VALUES is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < MINMAX_MAX_VALUES)
+	{
+		FmgrInfo   *cmpFn;
+		Datum	   *values;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												   BTLessStrategyNumber);
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances);
+
+	Assert(count_values(cranges, ncranges) <= MINMAX_MAX_VALUES * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+
+/*
+ * Compute distance between two abstime values.
+ *
+ * abstime values are int32 values, so just subtract them
+ */
+Datum
+brin_minmax_multi_distance_abstime(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	AbsoluteTime a = PG_GETARG_ABSOLUTETIME(0);
+	AbsoluteTime b = PG_GETARG_ABSOLUTETIME(1);
+
+	delta = ((double)b - (double)a);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+
+/*
+ * Compute distance between two reltime values.
+ *
+ * reltime values are int32 values, so just subtract them
+ */
+Datum
+brin_minmax_multi_distance_reltime(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	RelativeTime a = PG_GETARG_ABSOLUTETIME(0);
+	RelativeTime b = PG_GETARG_ABSOLUTETIME(1);
+
+	delta = ((double)b - (double)a);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	/*
+	 * If the new value is null, we record that we saw it if it's the first
+	 * one; otherwise, there's nothing to do.
+	 */
+	if (isnull)
+	{
+		if (column->bv_hasnulls)
+			PG_RETURN_BOOL(false);
+
+		column->bv_hasnulls = true;
+		PG_RETURN_BOOL(true);
+	}
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		ranges = minmax_multi_init();
+		column->bv_allnulls = false;
+		modified = true;
+	}
+	else
+	{
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized, attno, attr);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+	if (modified)
+	{
+		SerializedRanges *s = range_serialize(ranges, attno, attr);
+		column->bv_values[0] = PointerGetDatum(s);
+
+		/*
+		 * XXX pfree must happen after range_serialize, because the Ranges value
+		 * may reference the original serialized value.
+		 */
+		if (serialized)
+			pfree(serialized);
+	}
+
+	pfree(ranges);
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+	Form_pg_attribute attr;
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized, attno, attr);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+				
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+
+	/* Adjust "hasnulls" */
+	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+		col_a->bv_hasnulls = true;
+
+	/* If there are no values in B, there's nothing left to do */
+	if (col_b->bv_allnulls)
+		PG_RETURN_VOID();
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
+	 * B into A, and we're done.  We cannot run the operators in this case,
+	 * because values in A might contain garbage.  Note we already established
+	 * that B contains values.
+	 */
+	if (col_a->bv_allnulls)
+	{
+		col_a->bv_allnulls = false;
+		col_a->bv_values[0] = datumCopy(col_b->bv_values[0], false, -1);
+		PG_RETURN_VOID();
+	}
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a, attno, attr);
+	ranges_b = range_deserialize(serialized_b, attno, attr);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the MINMAX_MAX_VALUES
+	 * threshold. The collapsed ranges will be stored as a single value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attno, attr));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
diff --git a/src/include/catalog/pg_amop.h b/src/include/catalog/pg_amop.h
index 84dc6e3..be2eba8 100644
--- a/src/include/catalog/pg_amop.h
+++ b/src/include/catalog/pg_amop.h
@@ -974,6 +974,52 @@ DATA(insert (	4054	 23   20 2 s		80	  3580 0 ));
 DATA(insert (	4054	 23   20 3 s		15	  3580 0 ));
 DATA(insert (	4054	 23   20 4 s		82	  3580 0 ));
 DATA(insert (	4054	 23   20 5 s		76	  3580 0 ));
+/* minmax multi integer */
+DATA(insert (	4126	 20   20 1 s	   412	  3580 0 ));
+DATA(insert (	4126	 20   20 2 s	   414	  3580 0 ));
+DATA(insert (	4126	 20   20 3 s	   410	  3580 0 ));
+DATA(insert (	4126	 20   20 4 s	   415	  3580 0 ));
+DATA(insert (	4126	 20   20 5 s	   413	  3580 0 ));
+DATA(insert (	4126	 20   21 1 s	  1870	  3580 0 ));
+DATA(insert (	4126	 20   21 2 s	  1872	  3580 0 ));
+DATA(insert (	4126	 20   21 3 s	  1868	  3580 0 ));
+DATA(insert (	4126	 20   21 4 s	  1873	  3580 0 ));
+DATA(insert (	4126	 20   21 5 s	  1871	  3580 0 ));
+DATA(insert (	4126	 20   23 1 s	   418	  3580 0 ));
+DATA(insert (	4126	 20   23 2 s	   420	  3580 0 ));
+DATA(insert (	4126	 20   23 3 s	   416	  3580 0 ));
+DATA(insert (	4126	 20   23 4 s	   430	  3580 0 ));
+DATA(insert (	4126	 20   23 5 s	   419	  3580 0 ));
+DATA(insert (	4126	 21   21 1 s		95	  3580 0 ));
+DATA(insert (	4126	 21   21 2 s	   522	  3580 0 ));
+DATA(insert (	4126	 21   21 3 s		94	  3580 0 ));
+DATA(insert (	4126	 21   21 4 s	   524	  3580 0 ));
+DATA(insert (	4126	 21   21 5 s	   520	  3580 0 ));
+DATA(insert (	4126	 21   20 1 s	  1864	  3580 0 ));
+DATA(insert (	4126	 21   20 2 s	  1866	  3580 0 ));
+DATA(insert (	4126	 21   20 3 s	  1862	  3580 0 ));
+DATA(insert (	4126	 21   20 4 s	  1867	  3580 0 ));
+DATA(insert (	4126	 21   20 5 s	  1865	  3580 0 ));
+DATA(insert (	4126	 21   23 1 s	   534	  3580 0 ));
+DATA(insert (	4126	 21   23 2 s	   540	  3580 0 ));
+DATA(insert (	4126	 21   23 3 s	   532	  3580 0 ));
+DATA(insert (	4126	 21   23 4 s	   542	  3580 0 ));
+DATA(insert (	4126	 21   23 5 s	   536	  3580 0 ));
+DATA(insert (	4126	 23   23 1 s		97	  3580 0 ));
+DATA(insert (	4126	 23   23 2 s	   523	  3580 0 ));
+DATA(insert (	4126	 23   23 3 s		96	  3580 0 ));
+DATA(insert (	4126	 23   23 4 s	   525	  3580 0 ));
+DATA(insert (	4126	 23   23 5 s	   521	  3580 0 ));
+DATA(insert (	4126	 23   21 1 s	   535	  3580 0 ));
+DATA(insert (	4126	 23   21 2 s	   541	  3580 0 ));
+DATA(insert (	4126	 23   21 3 s	   533	  3580 0 ));
+DATA(insert (	4126	 23   21 4 s	   543	  3580 0 ));
+DATA(insert (	4126	 23   21 5 s	   537	  3580 0 ));
+DATA(insert (	4126	 23   20 1 s		37	  3580 0 ));
+DATA(insert (	4126	 23   20 2 s		80	  3580 0 ));
+DATA(insert (	4126	 23   20 3 s		15	  3580 0 ));
+DATA(insert (	4126	 23   20 4 s		82	  3580 0 ));
+DATA(insert (	4126	 23   20 5 s		76	  3580 0 ));
 /* bloom integer */
 DATA(insert (	5024	 20   20 1 s	   410	  3580 0 ));
 DATA(insert (	5024	 20   21 1 s	  1868	  3580 0 ));
@@ -999,6 +1045,12 @@ DATA(insert (	4068	 26   26 2 s	   611	  3580 0 ));
 DATA(insert (	4068	 26   26 3 s	   607	  3580 0 ));
 DATA(insert (	4068	 26   26 4 s	   612	  3580 0 ));
 DATA(insert (	4068	 26   26 5 s	   610	  3580 0 ));
+/* minmax oid */
+DATA(insert (	4127	 26   26 1 s	   609	  3580 0 ));
+DATA(insert (	4127	 26   26 2 s	   611	  3580 0 ));
+DATA(insert (	4127	 26   26 3 s	   607	  3580 0 ));
+DATA(insert (	4127	 26   26 4 s	   612	  3580 0 ));
+DATA(insert (	4127	 26   26 5 s	   610	  3580 0 ));
 /* bloom oid */
 DATA(insert (	5029	 26   26 1 s	   607	  3580 0 ));
 /* minmax tid */
@@ -1007,6 +1059,12 @@ DATA(insert (	4069	 27   27 2 s	  2801	  3580 0 ));
 DATA(insert (	4069	 27   27 3 s	   387	  3580 0 ));
 DATA(insert (	4069	 27   27 4 s	  2802	  3580 0 ));
 DATA(insert (	4069	 27   27 5 s	  2800	  3580 0 ));
+/* minmax multi tid */
+DATA(insert (	4128	 27   27 1 s	  2799	  3580 0 ));
+DATA(insert (	4128	 27   27 2 s	  2801	  3580 0 ));
+DATA(insert (	4128	 27   27 3 s	   387	  3580 0 ));
+DATA(insert (	4128	 27   27 4 s	  2802	  3580 0 ));
+DATA(insert (	4128	 27   27 5 s	  2800	  3580 0 ));
 /* minmax float (float4, float8) */
 DATA(insert (	4070	700  700 1 s	   622	  3580 0 ));
 DATA(insert (	4070	700  700 2 s	   624	  3580 0 ));
@@ -1028,6 +1086,27 @@ DATA(insert (	4070	701  701 2 s	   673	  3580 0 ));
 DATA(insert (	4070	701  701 3 s	   670	  3580 0 ));
 DATA(insert (	4070	701  701 4 s	   675	  3580 0 ));
 DATA(insert (	4070	701  701 5 s	   674	  3580 0 ));
+/* minmax multi (float8) */
+DATA(insert (	4005	700  700 1 s	   622	  3580 0 ));
+DATA(insert (	4005	700  700 2 s	   624	  3580 0 ));
+DATA(insert (	4005	700  700 3 s	   620	  3580 0 ));
+DATA(insert (	4005	700  700 4 s	   625	  3580 0 ));
+DATA(insert (	4005	700  700 5 s	   623	  3580 0 ));
+DATA(insert (	4005	700  701 1 s	  1122	  3580 0 ));
+DATA(insert (	4005	700  701 2 s	  1124	  3580 0 ));
+DATA(insert (	4005	700  701 3 s	  1120	  3580 0 ));
+DATA(insert (	4005	700  701 4 s	  1125	  3580 0 ));
+DATA(insert (	4005	700  701 5 s	  1123	  3580 0 ));
+DATA(insert (	4005	701  700 1 s	  1132	  3580 0 ));
+DATA(insert (	4005	701  700 2 s	  1134	  3580 0 ));
+DATA(insert (	4005	701  700 3 s	  1130	  3580 0 ));
+DATA(insert (	4005	701  700 4 s	  1135	  3580 0 ));
+DATA(insert (	4005	701  700 5 s	  1133	  3580 0 ));
+DATA(insert (	4005	701  701 1 s	   672	  3580 0 ));
+DATA(insert (	4005	701  701 2 s	   673	  3580 0 ));
+DATA(insert (	4005	701  701 3 s	   670	  3580 0 ));
+DATA(insert (	4005	701  701 4 s	   675	  3580 0 ));
+DATA(insert (	4005	701  701 5 s	   674	  3580 0 ));
 /* bloom float (float4, float8) */
 DATA(insert (	5030	700  700 1 s	   620	  3580 0 ));
 DATA(insert (	5030	700  701 1 s	  1120	  3580 0 ));
@@ -1040,6 +1119,12 @@ DATA(insert (	4072	702  702 2 s	   564	  3580 0 ));
 DATA(insert (	4072	702  702 3 s	   560	  3580 0 ));
 DATA(insert (	4072	702  702 4 s	   565	  3580 0 ));
 DATA(insert (	4072	702  702 5 s	   563	  3580 0 ));
+/* multi minmax abstime */
+DATA(insert (	4146	702  702 1 s	   562	  3580 0 ));
+DATA(insert (	4146	702  702 2 s	   564	  3580 0 ));
+DATA(insert (	4146	702  702 3 s	   560	  3580 0 ));
+DATA(insert (	4146	702  702 4 s	   565	  3580 0 ));
+DATA(insert (	4146	702  702 5 s	   563	  3580 0 ));
 /* bloom abstime */
 DATA(insert (	5031	702  702 1 s	   560	  3580 0 ));
 /* minmax reltime */
@@ -1048,6 +1133,12 @@ DATA(insert (	4073	703  703 2 s	   570	  3580 0 ));
 DATA(insert (	4073	703  703 3 s	   566	  3580 0 ));
 DATA(insert (	4073	703  703 4 s	   571	  3580 0 ));
 DATA(insert (	4073	703  703 5 s	   569	  3580 0 ));
+/* multi minmax reltime */
+DATA(insert (	4147	703  703 1 s	   568	  3580 0 ));
+DATA(insert (	4147	703  703 2 s	   570	  3580 0 ));
+DATA(insert (	4147	703  703 3 s	   566	  3580 0 ));
+DATA(insert (	4147	703  703 4 s	   571	  3580 0 ));
+DATA(insert (	4147	703  703 5 s	   569	  3580 0 ));
 /* bloom reltime */
 DATA(insert (	5032	703  703 1 s	   566	  3580 0 ));
 /* minmax macaddr */
@@ -1056,6 +1147,12 @@ DATA(insert (	4074	829  829 2 s	  1223	  3580 0 ));
 DATA(insert (	4074	829  829 3 s	  1220	  3580 0 ));
 DATA(insert (	4074	829  829 4 s	  1225	  3580 0 ));
 DATA(insert (	4074	829  829 5 s	  1224	  3580 0 ));
+/* multi minmax macaddr */
+DATA(insert (	4143	829  829 1 s	  1222	  3580 0 ));
+DATA(insert (	4143	829  829 2 s	  1223	  3580 0 ));
+DATA(insert (	4143	829  829 3 s	  1220	  3580 0 ));
+DATA(insert (	4143	829  829 4 s	  1225	  3580 0 ));
+DATA(insert (	4143	829  829 5 s	  1224	  3580 0 ));
 /* bloom macaddr */
 DATA(insert (	5033	829  829 1 s	  1220	  3580 0 ));
 /* minmax macaddr8 */
@@ -1064,6 +1161,12 @@ DATA(insert (	4109	774  774 2 s	  3365	  3580 0 ));
 DATA(insert (	4109	774  774 3 s	  3362	  3580 0 ));
 DATA(insert (	4109	774  774 4 s	  3367	  3580 0 ));
 DATA(insert (	4109	774  774 5 s	  3366	  3580 0 ));
+/* multi minmax macaddr8 */
+DATA(insert (	4144	774  774 1 s	  3364	  3580 0 ));
+DATA(insert (	4144	774  774 2 s	  3365	  3580 0 ));
+DATA(insert (	4144	774  774 3 s	  3362	  3580 0 ));
+DATA(insert (	4144	774  774 4 s	  3367	  3580 0 ));
+DATA(insert (	4144	774  774 5 s	  3366	  3580 0 ));
 /* bloom macaddr8 */
 DATA(insert (	5034	774  774 1 s	  3362	  3580 0 ));
 /* minmax inet */
@@ -1072,6 +1175,12 @@ DATA(insert (	4075	869  869 2 s	  1204	  3580 0 ));
 DATA(insert (	4075	869  869 3 s	  1201	  3580 0 ));
 DATA(insert (	4075	869  869 4 s	  1206	  3580 0 ));
 DATA(insert (	4075	869  869 5 s	  1205	  3580 0 ));
+/* multi minmax inet */
+DATA(insert (	4145	869  869 1 s	  1203	  3580 0 ));
+DATA(insert (	4145	869  869 2 s	  1204	  3580 0 ));
+DATA(insert (	4145	869  869 3 s	  1201	  3580 0 ));
+DATA(insert (	4145	869  869 4 s	  1206	  3580 0 ));
+DATA(insert (	4145	869  869 5 s	  1205	  3580 0 ));
 /* inclusion inet */
 DATA(insert (	4102	869  869 3 s	  3552	  3580 0 ));
 DATA(insert (	4102	869  869 7 s	   934	  3580 0 ));
@@ -1095,6 +1204,12 @@ DATA(insert (	4077   1083 1083 2 s	  1111	  3580 0 ));
 DATA(insert (	4077   1083 1083 3 s	  1108	  3580 0 ));
 DATA(insert (	4077   1083 1083 4 s	  1113	  3580 0 ));
 DATA(insert (	4077   1083 1083 5 s	  1112	  3580 0 ));
+/* multi minmax time without time zone */
+DATA(insert (	4135   1083 1083 1 s	  1110	  3580 0 ));
+DATA(insert (	4135   1083 1083 2 s	  1111	  3580 0 ));
+DATA(insert (	4135   1083 1083 3 s	  1108	  3580 0 ));
+DATA(insert (	4135   1083 1083 4 s	  1113	  3580 0 ));
+DATA(insert (	4135   1083 1083 5 s	  1112	  3580 0 ));
 /* bloom time without time zone */
 DATA(insert (	5037   1083 1083 1 s	  1108	  3580 0 ));
 /* minmax datetime (date, timestamp, timestamptz) */
@@ -1143,6 +1258,52 @@ DATA(insert (	4059   1184 1184 2 s	  1323	  3580 0 ));
 DATA(insert (	4059   1184 1184 3 s	  1320	  3580 0 ));
 DATA(insert (	4059   1184 1184 4 s	  1325	  3580 0 ));
 DATA(insert (	4059   1184 1184 5 s	  1324	  3580 0 ));
+/* minmax multi (timestamp, timestamptz) */
+DATA(insert (	4006   1114 1114 1 s	  2062	  3580 0 ));
+DATA(insert (	4006   1114 1114 2 s	  2063	  3580 0 ));
+DATA(insert (	4006   1114 1114 3 s	  2060	  3580 0 ));
+DATA(insert (	4006   1114 1114 4 s	  2065	  3580 0 ));
+DATA(insert (	4006   1114 1114 5 s	  2064	  3580 0 ));
+DATA(insert (	4006   1114 1082 1 s	  2371	  3580 0 ));
+DATA(insert (	4006   1114 1082 2 s	  2372	  3580 0 ));
+DATA(insert (	4006   1114 1082 3 s	  2373	  3580 0 ));
+DATA(insert (	4006   1114 1082 4 s	  2374	  3580 0 ));
+DATA(insert (	4006   1114 1082 5 s	  2375	  3580 0 ));
+DATA(insert (	4006   1114 1184 1 s	  2534	  3580 0 ));
+DATA(insert (	4006   1114 1184 2 s	  2535	  3580 0 ));
+DATA(insert (	4006   1114 1184 3 s	  2536	  3580 0 ));
+DATA(insert (	4006   1114 1184 4 s	  2537	  3580 0 ));
+DATA(insert (	4006   1114 1184 5 s	  2538	  3580 0 ));
+DATA(insert (	4006   1082 1082 1 s	  1095	  3580 0 ));
+DATA(insert (	4006   1082 1082 2 s	  1096	  3580 0 ));
+DATA(insert (	4006   1082 1082 3 s	  1093	  3580 0 ));
+DATA(insert (	4006   1082 1082 4 s	  1098	  3580 0 ));
+DATA(insert (	4006   1082 1082 5 s	  1097	  3580 0 ));
+DATA(insert (	4006   1082 1114 1 s	  2345	  3580 0 ));
+DATA(insert (	4006   1082 1114 2 s	  2346	  3580 0 ));
+DATA(insert (	4006   1082 1114 3 s	  2347	  3580 0 ));
+DATA(insert (	4006   1082 1114 4 s	  2348	  3580 0 ));
+DATA(insert (	4006   1082 1114 5 s	  2349	  3580 0 ));
+DATA(insert (	4006   1082 1184 1 s	  2358	  3580 0 ));
+DATA(insert (	4006   1082 1184 2 s	  2359	  3580 0 ));
+DATA(insert (	4006   1082 1184 3 s	  2360	  3580 0 ));
+DATA(insert (	4006   1082 1184 4 s	  2361	  3580 0 ));
+DATA(insert (	4006   1082 1184 5 s	  2362	  3580 0 ));
+DATA(insert (	4006   1184 1082 1 s	  2384	  3580 0 ));
+DATA(insert (	4006   1184 1082 2 s	  2385	  3580 0 ));
+DATA(insert (	4006   1184 1082 3 s	  2386	  3580 0 ));
+DATA(insert (	4006   1184 1082 4 s	  2387	  3580 0 ));
+DATA(insert (	4006   1184 1082 5 s	  2388	  3580 0 ));
+DATA(insert (	4006   1184 1114 1 s	  2540	  3580 0 ));
+DATA(insert (	4006   1184 1114 2 s	  2541	  3580 0 ));
+DATA(insert (	4006   1184 1114 3 s	  2542	  3580 0 ));
+DATA(insert (	4006   1184 1114 4 s	  2543	  3580 0 ));
+DATA(insert (	4006   1184 1114 5 s	  2544	  3580 0 ));
+DATA(insert (	4006   1184 1184 1 s	  1322	  3580 0 ));
+DATA(insert (	4006   1184 1184 2 s	  1323	  3580 0 ));
+DATA(insert (	4006   1184 1184 3 s	  1320	  3580 0 ));
+DATA(insert (	4006   1184 1184 4 s	  1325	  3580 0 ));
+DATA(insert (	4006   1184 1184 5 s	  1324	  3580 0 ));
 /* bloom datetime (date, timestamp, timestamptz) */
 DATA(insert (	5038   1114 1114 1 s	  2060	  3580 0 ));
 DATA(insert (	5038   1114 1082 1 s	  2373	  3580 0 ));
@@ -1160,6 +1321,12 @@ DATA(insert (	4078   1186 1186 2 s	  1333	  3580 0 ));
 DATA(insert (	4078   1186 1186 3 s	  1330	  3580 0 ));
 DATA(insert (	4078   1186 1186 4 s	  1335	  3580 0 ));
 DATA(insert (	4078   1186 1186 5 s	  1334	  3580 0 ));
+/* multi minmax interval */
+DATA(insert (	4134   1186 1186 1 s	  1332	  3580 0 ));
+DATA(insert (	4134   1186 1186 2 s	  1333	  3580 0 ));
+DATA(insert (	4134   1186 1186 3 s	  1330	  3580 0 ));
+DATA(insert (	4134   1186 1186 4 s	  1335	  3580 0 ));
+DATA(insert (	4134   1186 1186 5 s	  1334	  3580 0 ));
 /* bloom interval */
 DATA(insert (	5041   1186 1186 1 s	  1330	  3580 0 ));
 /* minmax time with time zone */
@@ -1168,6 +1335,12 @@ DATA(insert (	4058   1266 1266 2 s	  1553	  3580 0 ));
 DATA(insert (	4058   1266 1266 3 s	  1550	  3580 0 ));
 DATA(insert (	4058   1266 1266 4 s	  1555	  3580 0 ));
 DATA(insert (	4058   1266 1266 5 s	  1554	  3580 0 ));
+/* multi minmax time with time zone */
+DATA(insert (	4133   1266 1266 1 s	  1552	  3580 0 ));
+DATA(insert (	4133   1266 1266 2 s	  1553	  3580 0 ));
+DATA(insert (	4133   1266 1266 3 s	  1550	  3580 0 ));
+DATA(insert (	4133   1266 1266 4 s	  1555	  3580 0 ));
+DATA(insert (	4133   1266 1266 5 s	  1554	  3580 0 ));
 /* bloom time with time zone */
 DATA(insert (	5042   1266 1266 1 s	  1550	  3580 0 ));
 /* minmax bit */
@@ -1188,6 +1361,12 @@ DATA(insert (	4055   1700 1700 2 s	  1755	  3580 0 ));
 DATA(insert (	4055   1700 1700 3 s	  1752	  3580 0 ));
 DATA(insert (	4055   1700 1700 4 s	  1757	  3580 0 ));
 DATA(insert (	4055   1700 1700 5 s	  1756	  3580 0 ));
+/* minmax multi numeric */
+DATA(insert (	4008   1700 1700 1 s	  1754	  3580 0 ));
+DATA(insert (	4008   1700 1700 2 s	  1755	  3580 0 ));
+DATA(insert (	4008   1700 1700 3 s	  1752	  3580 0 ));
+DATA(insert (	4008   1700 1700 4 s	  1757	  3580 0 ));
+DATA(insert (	4008   1700 1700 5 s	  1756	  3580 0 ));
 /* bloom numeric */
 DATA(insert (	5045   1700 1700 1 s	  1752	  3580 0 ));
 /* minmax uuid */
@@ -1196,6 +1375,12 @@ DATA(insert (	4081   2950 2950 2 s	  2976	  3580 0 ));
 DATA(insert (	4081   2950 2950 3 s	  2972	  3580 0 ));
 DATA(insert (	4081   2950 2950 4 s	  2977	  3580 0 ));
 DATA(insert (	4081   2950 2950 5 s	  2975	  3580 0 ));
+/* minmax multi uuid */
+DATA(insert (	4131   2950 2950 1 s	  2974	  3580 0 ));
+DATA(insert (	4131   2950 2950 2 s	  2976	  3580 0 ));
+DATA(insert (	4131   2950 2950 3 s	  2972	  3580 0 ));
+DATA(insert (	4131   2950 2950 4 s	  2977	  3580 0 ));
+DATA(insert (	4131   2950 2950 5 s	  2975	  3580 0 ));
 /* bloom uuid */
 DATA(insert (	5046   2950 2950 1 s	  2972	  3580 0 ));
 /* inclusion range types */
@@ -1219,6 +1404,12 @@ DATA(insert (	4082   3220 3220 2 s	  3226	  3580 0 ));
 DATA(insert (	4082   3220 3220 3 s	  3222	  3580 0 ));
 DATA(insert (	4082   3220 3220 4 s	  3227	  3580 0 ));
 DATA(insert (	4082   3220 3220 5 s	  3225	  3580 0 ));
+/* multi minmax pg_lsn */
+DATA(insert (	4132   3220 3220 1 s	  3224	  3580 0 ));
+DATA(insert (	4132   3220 3220 2 s	  3226	  3580 0 ));
+DATA(insert (	4132   3220 3220 3 s	  3222	  3580 0 ));
+DATA(insert (	4132   3220 3220 4 s	  3227	  3580 0 ));
+DATA(insert (	4132   3220 3220 5 s	  3225	  3580 0 ));
 /* bloom pg_lsn */
 DATA(insert (	5047   3220 3220 1 s	  3222	  3580 0 ));
 /* inclusion box */
diff --git a/src/include/catalog/pg_amproc.h b/src/include/catalog/pg_amproc.h
index 4871836..8fb3405 100644
--- a/src/include/catalog/pg_amproc.h
+++ b/src/include/catalog/pg_amproc.h
@@ -415,6 +415,55 @@ DATA(insert (	4054	23	  21  2  3384 ));
 DATA(insert (	4054	23	  21  3  3385 ));
 DATA(insert (	4054	23	  21  4  3386 ));
 
+/* minmax multi integer: int2, int4, int8 */
+DATA(insert (	4126	20	  20  1  4001 ));
+DATA(insert (	4126	20	  20  2  4002 ));
+DATA(insert (	4126	20	  20  3  4003 ));
+DATA(insert (	4126	20	  20  4  4004 ));
+DATA(insert (	4126	20	  20  11 4013 ));
+DATA(insert (	4126	20	  21  1  4001 ));
+DATA(insert (	4126	20	  21  2  4002 ));
+DATA(insert (	4126	20	  21  3  4003 ));
+DATA(insert (	4126	20	  21  4  4004 ));
+DATA(insert (	4126	20	  21  11 4013 ));
+DATA(insert (	4126	20	  23  1  4001 ));
+DATA(insert (	4126	20	  23  2  4002 ));
+DATA(insert (	4126	20	  23  3  4003 ));
+DATA(insert (	4126	20	  23  4  4004 ));
+DATA(insert (	4126	20	  23  11 4013 ));
+
+DATA(insert (	4126	21	  21  1  4001 ));
+DATA(insert (	4126	21	  21  2  4002 ));
+DATA(insert (	4126	21	  21  3  4003 ));
+DATA(insert (	4126	21	  21  4  4004 ));
+DATA(insert (	4126	21	  21  11 4011 ));
+DATA(insert (	4126	21	  20  1  4001 ));
+DATA(insert (	4126	21	  20  2  4002 ));
+DATA(insert (	4126	21	  20  3  4003 ));
+DATA(insert (	4126	21	  20  4  4004 ));
+DATA(insert (	4126	21	  20  11 4011 ));
+DATA(insert (	4126	21	  23  1  4001 ));
+DATA(insert (	4126	21	  23  2  4002 ));
+DATA(insert (	4126	21	  23  3  4003 ));
+DATA(insert (	4126	21	  23  4  4004 ));
+DATA(insert (	4126	21	  23  11 4011 ));
+
+DATA(insert (	4126	23	  23  1  4001 ));
+DATA(insert (	4126	23	  23  2  4002 ));
+DATA(insert (	4126	23	  23  3  4003 ));
+DATA(insert (	4126	23	  23  4  4004 ));
+DATA(insert (	4126	23	  23  11 4012 ));
+DATA(insert (	4126	23	  20  1  4001 ));
+DATA(insert (	4126	23	  20  2  4002 ));
+DATA(insert (	4126	23	  20  3  4003 ));
+DATA(insert (	4126	23	  20  4  4004 ));
+DATA(insert (	4126	23	  20  11 4012 ));
+DATA(insert (	4126	23	  21  1  4001 ));
+DATA(insert (	4126	23	  21  2  4002 ));
+DATA(insert (	4126	23	  21  3  4003 ));
+DATA(insert (	4126	23	  21  4  4004 ));
+DATA(insert (	4126	23	  21  11 4012 ));
+
 /* bloom integer: int2, int4, int8 */
 DATA(insert (	5024	20	  20  1  5017 ));
 DATA(insert (	5024	20	  20  2  5018 ));
@@ -450,6 +499,12 @@ DATA(insert (	4068	26	  26  1  3383 ));
 DATA(insert (	4068	26	  26  2  3384 ));
 DATA(insert (	4068	26	  26  3  3385 ));
 DATA(insert (	4068	26	  26  4  3386 ));
+/* minmax multi oid */
+DATA(insert (	4127	26	  26  1  4001 ));
+DATA(insert (	4127	26	  26  2  4002 ));
+DATA(insert (	4127	26	  26  3  4003 ));
+DATA(insert (	4127	26	  26  4  4004 ));
+DATA(insert (	4127	26	  26 11  4012 ));
 /* bloom oid */
 DATA(insert (	5029	26	  26  1  5017 ));
 DATA(insert (	5029	26	  26  2  5018 ));
@@ -461,6 +516,12 @@ DATA(insert (	4069	27	  27  1  3383 ));
 DATA(insert (	4069	27	  27  2  3384 ));
 DATA(insert (	4069	27	  27  3  3385 ));
 DATA(insert (	4069	27	  27  4  3386 ));
+/* minmax multi tid */
+DATA(insert (	4128	27	  27  1  4001 ));
+DATA(insert (	4128	27	  27  2  4002 ));
+DATA(insert (	4128	27	  27  3  4003 ));
+DATA(insert (	4128	27	  27  4  4004 ));
+DATA(insert (	4128	27	  27  11 4129 ));
 /* minmax float */
 DATA(insert (	4070   700	 700  1  3383 ));
 DATA(insert (	4070   700	 700  2  3384 ));
@@ -482,6 +543,31 @@ DATA(insert (	4070   701	 700  2  3384 ));
 DATA(insert (	4070   701	 700  3  3385 ));
 DATA(insert (	4070   701	 700  4  3386 ));
 
+/* minmax multi float */
+DATA(insert (	4005   700	 700  1  4001 ));
+DATA(insert (	4005   700	 700  2  4002 ));
+DATA(insert (	4005   700	 700  3  4003 ));
+DATA(insert (	4005   700	 700  4  4004 ));
+DATA(insert (	4005   700	 700  11 4010 ));
+
+DATA(insert (	4005   700	 701  1  4001 ));
+DATA(insert (	4005   700	 701  2  4002 ));
+DATA(insert (	4005   700	 701  3  4003 ));
+DATA(insert (	4005   700	 701  4  4004 ));
+DATA(insert (	4005   700	 701  11 4010 ));
+
+DATA(insert (	4005   701	 701  1  4001 ));
+DATA(insert (	4005   701	 701  2  4002 ));
+DATA(insert (	4005   701	 701  3  4003 ));
+DATA(insert (	4005   701	 701  4  4004 ));
+DATA(insert (	4005   701	 701  11 4007 ));
+
+DATA(insert (	4005   701	 700  1  4001 ));
+DATA(insert (	4005   701	 700  2  4002 ));
+DATA(insert (	4005   701	 700  3  4003 ));
+DATA(insert (	4005   701	 700  4  4004 ));
+DATA(insert (	4005   701	 700  11 4007 ));
+
 /* bloom float */
 DATA(insert (	5030   700	 700  1  5017 ));
 DATA(insert (	5030   700	 700  2  5018 ));
@@ -500,6 +586,12 @@ DATA(insert (	4072   702	 702  1  3383 ));
 DATA(insert (	4072   702	 702  2  3384 ));
 DATA(insert (	4072   702	 702  3  3385 ));
 DATA(insert (	4072   702	 702  4  3386 ));
+/* multi minmax abstime */
+DATA(insert (	4146   702	 702  1  4001 ));
+DATA(insert (	4146   702	 702  2  4002 ));
+DATA(insert (	4146   702	 702  3  4003 ));
+DATA(insert (	4146   702	 702  4  4004 ));
+DATA(insert (	4146   702	 702 11  4148 ));
 /* bloom abstime */
 DATA(insert (	5031   702	 702  1  5017 ));
 DATA(insert (	5031   702	 702  2  5018 ));
@@ -511,6 +603,12 @@ DATA(insert (	4073   703	 703  1  3383 ));
 DATA(insert (	4073   703	 703  2  3384 ));
 DATA(insert (	4073   703	 703  3  3385 ));
 DATA(insert (	4073   703	 703  4  3386 ));
+/* multi minmax reltime */
+DATA(insert (	4147   703	 703  1  4001 ));
+DATA(insert (	4147   703	 703  2  4002 ));
+DATA(insert (	4147   703	 703  3  4003 ));
+DATA(insert (	4147   703	 703  4  4004 ));
+DATA(insert (	4147   703	 703 11  4149 ));
 /* bloom reltime */
 DATA(insert (	5032   703	 703  1  5017 ));
 DATA(insert (	5032   703	 703  2  5018 ));
@@ -522,6 +620,12 @@ DATA(insert (	4074   829	 829  1  3383 ));
 DATA(insert (	4074   829	 829  2  3384 ));
 DATA(insert (	4074   829	 829  3  3385 ));
 DATA(insert (	4074   829	 829  4  3386 ));
+/* multi minmax macaddr */
+DATA(insert (	4143   829	 829  1  4001 ));
+DATA(insert (	4143   829	 829  2  4002 ));
+DATA(insert (	4143   829	 829  3  4003 ));
+DATA(insert (	4143   829	 829  4  4004 ));
+DATA(insert (	4143   829	 829 11  4140 ));
 /* bloom macaddr */
 DATA(insert (	5033   829	 829  1  5017 ));
 DATA(insert (	5033   829	 829  2  5018 ));
@@ -533,6 +637,12 @@ DATA(insert (	4109   774	 774  1  3383 ));
 DATA(insert (	4109   774	 774  2  3384 ));
 DATA(insert (	4109   774	 774  3  3385 ));
 DATA(insert (	4109   774	 774  4  3386 ));
+/* minmax macaddr8 */
+DATA(insert (	4144   774	 774  1  4001 ));
+DATA(insert (	4144   774	 774  2  4002 ));
+DATA(insert (	4144   774	 774  3  4003 ));
+DATA(insert (	4144   774	 774  4  4004 ));
+DATA(insert (	4144   774	 774 11  4141 ));
 /* bloom macaddr8 */
 DATA(insert (	5034   774	 774  1  5017 ));
 DATA(insert (	5034   774	 774  2  5018 ));
@@ -544,6 +654,12 @@ DATA(insert (	4075   869	 869  1  3383 ));
 DATA(insert (	4075   869	 869  2  3384 ));
 DATA(insert (	4075   869	 869  3  3385 ));
 DATA(insert (	4075   869	 869  4  3386 ));
+/* multi minmax inet */
+DATA(insert (	4145   869	 869  1  4001 ));
+DATA(insert (	4145   869	 869  2  4002 ));
+DATA(insert (	4145   869	 869  3  4003 ));
+DATA(insert (	4145   869	 869  4  4004 ));
+DATA(insert (	4145   869	 869 11  4142 ));
 /* inclusion inet */
 DATA(insert (	4102   869	 869  1  4105 ));
 DATA(insert (	4102   869	 869  2  4106 ));
@@ -574,6 +690,12 @@ DATA(insert (	4077  1083	1083  1  3383 ));
 DATA(insert (	4077  1083	1083  2  3384 ));
 DATA(insert (	4077  1083	1083  3  3385 ));
 DATA(insert (	4077  1083	1083  4  3386 ));
+/* multi minmax time without time zone */
+DATA(insert (	4135  1083	1083  1  4001 ));
+DATA(insert (	4135  1083	1083  2  4002 ));
+DATA(insert (	4135  1083	1083  3  4003 ));
+DATA(insert (	4135  1083	1083  4  4004 ));
+DATA(insert (	4135  1083	1083 11  4136 ));
 /* bloom time without time zone */
 DATA(insert (	5037  1083	1083  1  5017 ));
 DATA(insert (	5037  1083	1083  2  5018 ));
@@ -620,6 +742,55 @@ DATA(insert (	4059  1082	1184  2  3384 ));
 DATA(insert (	4059  1082	1184  3  3385 ));
 DATA(insert (	4059  1082	1184  4  3386 ));
 
+/* minmax multi (date, timestamp, timestamptz) */
+DATA(insert (	4006  1114	1114  1  4001 ));
+DATA(insert (	4006  1114	1114  2  4002 ));
+DATA(insert (	4006  1114	1114  3  4003 ));
+DATA(insert (	4006  1114	1114  4  4004 ));
+DATA(insert (	4006  1114	1114  11 4007 ));
+DATA(insert (	4006  1114	1184  1  4001 ));
+DATA(insert (	4006  1114	1184  2  4002 ));
+DATA(insert (	4006  1114	1184  3  4003 ));
+DATA(insert (	4006  1114	1184  4  4004 ));
+DATA(insert (	4006  1114	1184  11 4007 ));
+DATA(insert (	4006  1114	1082  1  4001 ));
+DATA(insert (	4006  1114	1082  2  4002 ));
+DATA(insert (	4006  1114	1082  3  4003 ));
+DATA(insert (	4006  1114	1082  4  4004 ));
+DATA(insert (	4006  1114	1082  11 4007 ));
+
+DATA(insert (	4006  1184	1184  1  4001 ));
+DATA(insert (	4006  1184	1184  2  4002 ));
+DATA(insert (	4006  1184	1184  3  4003 ));
+DATA(insert (	4006  1184	1184  4  4004 ));
+DATA(insert (	4006  1184	1184  11 4007 ));
+DATA(insert (	4006  1184	1114  1  4001 ));
+DATA(insert (	4006  1184	1114  2  4002 ));
+DATA(insert (	4006  1184	1114  3  4003 ));
+DATA(insert (	4006  1184	1114  4  4004 ));
+DATA(insert (	4006  1184	1114  11 4007 ));
+DATA(insert (	4006  1184	1082  1  4001 ));
+DATA(insert (	4006  1184	1082  2  4002 ));
+DATA(insert (	4006  1184	1082  3  4003 ));
+DATA(insert (	4006  1184	1082  4  4004 ));
+DATA(insert (	4006  1184	1082  11 4007 ));
+
+DATA(insert (	4006  1082	1082  1  4001 ));
+DATA(insert (	4006  1082	1082  2  4002 ));
+DATA(insert (	4006  1082	1082  3  4003 ));
+DATA(insert (	4006  1082	1082  4  4004 ));
+DATA(insert (	4006  1082	1082  11 4007 ));
+DATA(insert (	4006  1082	1114  1  4001 ));
+DATA(insert (	4006  1082	1114  2  4002 ));
+DATA(insert (	4006  1082	1114  3  4003 ));
+DATA(insert (	4006  1082	1114  4  4004 ));
+DATA(insert (	4006  1082	1114  11 4007 ));
+DATA(insert (	4006  1082	1184  1  4001 ));
+DATA(insert (	4006  1082	1184  2  4002 ));
+DATA(insert (	4006  1082	1184  3  4003 ));
+DATA(insert (	4006  1082	1184  4  4004 ));
+DATA(insert (	4006  1082	1184  11 4007 ));
+
 /* bloom datetime (date, timestamp, timestamptz) */
 DATA(insert (	5038  1114	1114  1  5017 ));
 DATA(insert (	5038  1114	1114  2  5018 ));
@@ -644,6 +815,12 @@ DATA(insert (	4078  1186	1186  1  3383 ));
 DATA(insert (	4078  1186	1186  2  3384 ));
 DATA(insert (	4078  1186	1186  3  3385 ));
 DATA(insert (	4078  1186	1186  4  3386 ));
+/* multi minmax interval */
+DATA(insert (	4134  1186	1186  1  4001 ));
+DATA(insert (	4134  1186	1186  2  4002 ));
+DATA(insert (	4134  1186	1186  3  4003 ));
+DATA(insert (	4134  1186	1186  4  4004 ));
+DATA(insert (	4134  1186	1186 11  4137 ));
 /* bloom interval */
 DATA(insert (	5041  1186	1186  1  5017 ));
 DATA(insert (	5041  1186	1186  2  5018 ));
@@ -655,6 +832,12 @@ DATA(insert (	4058  1266	1266  1  3383 ));
 DATA(insert (	4058  1266	1266  2  3384 ));
 DATA(insert (	4058  1266	1266  3  3385 ));
 DATA(insert (	4058  1266	1266  4  3386 ));
+/* multi minmax time with time zone */
+DATA(insert (	4133  1266	1266  1  4001 ));
+DATA(insert (	4133  1266	1266  2  4002 ));
+DATA(insert (	4133  1266	1266  3  4003 ));
+DATA(insert (	4133  1266	1266  4  4004 ));
+DATA(insert (	4133  1266	1266 11  4138 ));
 /* bloom time with time zone */
 DATA(insert (	5042  1266	1266  1  5017 ));
 DATA(insert (	5042  1266	1266  2  5018 ));
@@ -676,6 +859,12 @@ DATA(insert (	4055  1700	1700  1  3383 ));
 DATA(insert (	4055  1700	1700  2  3384 ));
 DATA(insert (	4055  1700	1700  3  3385 ));
 DATA(insert (	4055  1700	1700  4  3386 ));
+/* minmax multi numeric */
+DATA(insert (	4008  1700	1700  1  4001 ));
+DATA(insert (	4008  1700	1700  2  4002 ));
+DATA(insert (	4008  1700	1700  3  4003 ));
+DATA(insert (	4008  1700	1700  4  4004 ));
+DATA(insert (	4008  1700	1700 11  4009 ));
 /* bloom numeric */
 DATA(insert (	5045  1700	1700  1  5017 ));
 DATA(insert (	5045  1700	1700  2  5018 ));
@@ -687,6 +876,12 @@ DATA(insert (	4081  2950	2950  1  3383 ));
 DATA(insert (	4081  2950	2950  2  3384 ));
 DATA(insert (	4081  2950	2950  3  3385 ));
 DATA(insert (	4081  2950	2950  4  3386 ));
+/* minmax multi uuid */
+DATA(insert (	4131  2950	2950  1  4001 ));
+DATA(insert (	4131  2950	2950  2  4002 ));
+DATA(insert (	4131  2950	2950  3  4003 ));
+DATA(insert (	4131  2950	2950  4  4004 ));
+DATA(insert (	4131  2950	2950  11 4130 ));
 /* bloom uuid */
 DATA(insert (	5046  2950	2950  1  5017 ));
 DATA(insert (	5046  2950	2950  2  5018 ));
@@ -706,6 +901,12 @@ DATA(insert (	4082  3220	3220  1  3383 ));
 DATA(insert (	4082  3220	3220  2  3384 ));
 DATA(insert (	4082  3220	3220  3  3385 ));
 DATA(insert (	4082  3220	3220  4  3386 ));
+/* multi minmax pg_lsn */
+DATA(insert (	4132  3220	3220  1  4001 ));
+DATA(insert (	4132  3220	3220  2  4002 ));
+DATA(insert (	4132  3220	3220  3  4003 ));
+DATA(insert (	4132  3220	3220  4  4004 ));
+DATA(insert (	4132  3220	3220 11  4139 ));
 /* bloom pg_lsn */
 DATA(insert (	5047  3220	3220  1  5017 ));
 DATA(insert (	5047  3220	3220  2  5018 ));
diff --git a/src/include/catalog/pg_opclass.h b/src/include/catalog/pg_opclass.h
index 7dbd4ff..4e102e4 100644
--- a/src/include/catalog/pg_opclass.h
+++ b/src/include/catalog/pg_opclass.h
@@ -220,54 +220,75 @@ DATA(insert (	3580	char_bloom_ops			PGNSP PGUID 5022	18 f 18 ));
 DATA(insert (	3580	name_minmax_ops			PGNSP PGUID 4065	19 t 19 ));
 DATA(insert (	3580	name_bloom_ops			PGNSP PGUID 5023	19 f 19 ));
 DATA(insert (	3580	int8_minmax_ops			PGNSP PGUID 4054	20 t 20 ));
+DATA(insert (	3580	int8_minmax_multi_ops	PGNSP PGUID 4126	20 f 20 ));
 DATA(insert (	3580	int8_bloom_ops			PGNSP PGUID 5024	20 f 20 ));
 DATA(insert (	3580	int2_minmax_ops			PGNSP PGUID 4054	21 t 21 ));
+DATA(insert (	3580	int2_minmax_multi_ops	PGNSP PGUID 4126	21 f 21 ));
 DATA(insert (	3580	int2_bloom_ops			PGNSP PGUID 5024	21 f 21 ));
 DATA(insert (	3580	int4_minmax_ops			PGNSP PGUID 4054	23 t 23 ));
+DATA(insert (	3580	int4_minmax_multi_ops	PGNSP PGUID 4126	23 f 23 ));
 DATA(insert (	3580	int4_bloom_ops			PGNSP PGUID 5024	23 f 23 ));
 DATA(insert (	3580	text_minmax_ops			PGNSP PGUID 4056	25 t 25 ));
 DATA(insert (	3580	text_bloom_ops			PGNSP PGUID 5027	25 f 25 ));
 DATA(insert (	3580	oid_minmax_ops			PGNSP PGUID 4068	26 t 26 ));
+DATA(insert (	3580	oid_minmax_multi_ops	PGNSP PGUID 4127	26 f 26 ));
 DATA(insert (	3580	oid_bloom_ops			PGNSP PGUID 5029	26 f 26 ));
 DATA(insert (	3580	tid_minmax_ops			PGNSP PGUID 4069	27 t 27 ));
+DATA(insert (	3580	tid_minmax_multi_ops	PGNSP PGUID 4128	27 f 27 ));
 DATA(insert (	3580	float4_minmax_ops		PGNSP PGUID 4070   700 t 700 ));
+DATA(insert (	3580	float4_minmax_multi_ops	PGNSP PGUID 4005   700 f 700 ));
 DATA(insert (	3580	float4_bloom_ops		PGNSP PGUID 5030   700 f 700 ));
 DATA(insert (	3580	float8_minmax_ops		PGNSP PGUID 4070   701 t 701 ));
+DATA(insert (	3580	float8_minmax_multi_ops	PGNSP PGUID 4005   701 f 701 ));
 DATA(insert (	3580	float8_bloom_ops		PGNSP PGUID 5030   701 f 701 ));
 DATA(insert (	3580	abstime_minmax_ops		PGNSP PGUID 4072   702 t 702 ));
+DATA(insert (	3580	abstime_minmax_multi_ops	PGNSP PGUID 4146   702 f 702 ));
 DATA(insert (	3580	abstime_bloom_ops		PGNSP PGUID 5031   702 f 702 ));
 DATA(insert (	3580	reltime_minmax_ops		PGNSP PGUID 4073   703 t 703 ));
+DATA(insert (	3580	reltime_minmax_multi_ops	PGNSP PGUID 4147   703 f 703 ));
 DATA(insert (	3580	reltime_bloom_ops		PGNSP PGUID 5032   703 f 703 ));
 DATA(insert (	3580	macaddr_minmax_ops		PGNSP PGUID 4074   829 t 829 ));
+DATA(insert (	3580	macaddr_minmax_multi_ops	PGNSP PGUID 4143   829 f 829 ));
 DATA(insert (	3580	macaddr_bloom_ops		PGNSP PGUID 5033   829 f 829 ));
 DATA(insert (	3580	macaddr8_minmax_ops		PGNSP PGUID 4109   774 t 774 ));
+DATA(insert (	3580	macaddr8_minmax_multi_ops	PGNSP PGUID 4144   774 f 774 ));
 DATA(insert (	3580	macaddr8_bloom_ops		PGNSP PGUID 5034   774 f 774 ));
 DATA(insert (	3580	inet_minmax_ops			PGNSP PGUID 4075   869 f 869 ));
+DATA(insert (	3580	inet_minmax_multi_ops	PGNSP PGUID 4145   869 f 869 ));
 DATA(insert (	3580	inet_inclusion_ops		PGNSP PGUID 4102   869 t 869 ));
 DATA(insert (	3580	inet_bloom_ops			PGNSP PGUID 5035   869 f 869 ));
 DATA(insert (	3580	bpchar_minmax_ops		PGNSP PGUID 4076  1042 t 1042 ));
 DATA(insert (	3580	bpchar_bloom_ops		PGNSP PGUID 5036  1042 f 1042 ));
 DATA(insert (	3580	time_minmax_ops			PGNSP PGUID 4077  1083 t 1083 ));
+DATA(insert (	3580	time_minmax_multi_ops	PGNSP PGUID 4135  1083 f 1083 ));
 DATA(insert (	3580	time_bloom_ops			PGNSP PGUID 5037  1083 f 1083 ));
 DATA(insert (	3580	date_minmax_ops			PGNSP PGUID 4059  1082 t 1082 ));
+DATA(insert (	3580	date_minmax_multi_ops	PGNSP PGUID 4006  1082 f 1082 ));
 DATA(insert (	3580	date_bloom_ops			PGNSP PGUID 5038  1082 f 1082 ));
 DATA(insert (	3580	timestamp_minmax_ops	PGNSP PGUID 4059  1114 t 1114 ));
+DATA(insert (	3580	timestamp_minmax_multi_ops	PGNSP PGUID 4006  1114 f 1114 ));
 DATA(insert (	3580	timestamp_bloom_ops		PGNSP PGUID 5038  1114 f 1114 ));
 DATA(insert (	3580	timestamptz_minmax_ops	PGNSP PGUID 4059  1184 t 1184 ));
+DATA(insert (	3580	timestamptz_minmax_multi_ops	PGNSP PGUID 4006  1184 f 1184 ));
 DATA(insert (	3580	timestamptz_bloom_ops	PGNSP PGUID 5038  1184 f 1184 ));
 DATA(insert (	3580	interval_minmax_ops		PGNSP PGUID 4078  1186 t 1186 ));
+DATA(insert (	3580	interval_minmax_multi_ops	PGNSP PGUID 4134  1186 f 1186 ));
 DATA(insert (	3580	interval_bloom_ops		PGNSP PGUID 5041  1186 f 1186 ));
 DATA(insert (	3580	timetz_minmax_ops		PGNSP PGUID 4058  1266 t 1266 ));
+DATA(insert (	3580	timetz_minmax_multi_ops	PGNSP PGUID 4133  1266 f 1266 ));
 DATA(insert (	3580	timetz_bloom_ops		PGNSP PGUID 5042  1266 f 1266 ));
 DATA(insert (	3580	bit_minmax_ops			PGNSP PGUID 4079  1560 t 1560 ));
 DATA(insert (	3580	varbit_minmax_ops		PGNSP PGUID 4080  1562 t 1562 ));
 DATA(insert (	3580	numeric_minmax_ops		PGNSP PGUID 4055  1700 t 1700 ));
+DATA(insert (	3580	numeric_minmax_multi_ops	PGNSP PGUID 4008  1700 f 1700 ));
 DATA(insert (	3580	numeric_bloom_ops		PGNSP PGUID 5045  1700 f 1700 ));
 /* no brin opclass for record, anyarray */
 DATA(insert (	3580	uuid_minmax_ops			PGNSP PGUID 4081  2950 t 2950 ));
+DATA(insert (	3580	uuid_minmax_multi_ops	PGNSP PGUID 4131  2950 f 2950 ));
 DATA(insert (	3580	uuid_bloom_ops			PGNSP PGUID 5046  2950 f 2950 ));
 DATA(insert (	3580	range_inclusion_ops		PGNSP PGUID 4103  3831 t 3831 ));
 DATA(insert (	3580	pg_lsn_minmax_ops		PGNSP PGUID 4082  3220 t 3220 ));
+DATA(insert (	3580	pg_lsn_minmax_multi_ops	PGNSP PGUID 4132  3220 f 3220 ));
 DATA(insert (	3580	pg_lsn_bloom_ops		PGNSP PGUID 5047  3220 f 3220 ));
 /* no brin opclass for enum, tsvector, tsquery, jsonb */
 DATA(insert (	3580	box_inclusion_ops		PGNSP PGUID 4104   603 t 603 ));
diff --git a/src/include/catalog/pg_opfamily.h b/src/include/catalog/pg_opfamily.h
index bc22ce3..f0ce10b 100644
--- a/src/include/catalog/pg_opfamily.h
+++ b/src/include/catalog/pg_opfamily.h
@@ -160,14 +160,18 @@ DATA(insert OID = 4036 (	2742	jsonb_ops		PGNSP PGUID ));
 DATA(insert OID = 4037 (	2742	jsonb_path_ops	PGNSP PGUID ));
 
 DATA(insert OID = 4054 (	3580	integer_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4126 (	3580	integer_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5024 (	3580	integer_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4055 (	3580	numeric_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4008 (	3580	numeric_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5045 (	3580	numeric_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4056 (	3580	text_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 5027 (	3580	text_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4058 (	3580	timetz_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4133 (	3580	timetz_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5042 (	3580	timetz_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4059 (	3580	datetime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4006 (	3580	datetime_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5038 (	3580	datetime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4062 (	3580	char_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 5022 (	3580	char_bloom_ops			PGNSP PGUID ));
@@ -176,33 +180,45 @@ DATA(insert OID = 5021 (	3580	bytea_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4065 (	3580	name_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 5023 (	3580	name_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4068 (	3580	oid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 4127 (	3580	oid_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5029 (	3580	oid_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4069 (	3580	tid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 4128 (	3580	tid_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 4070 (	3580	float_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4005 (	3580	float_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5030 (	3580	float_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4072 (	3580	abstime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4146 (	3580	abstime_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5031 (	3580	abstime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4073 (	3580	reltime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4147 (	3580	reltime_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5032 (	3580	reltime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4074 (	3580	macaddr_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4143 (	3580	macaddr_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5033 (	3580	macaddr_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4109 (	3580	macaddr8_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4144 (	3580	macaddr8_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5034 (	3580	macaddr8_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4075 (	3580	network_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4145 (	3580	network_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 4102 (	3580	network_inclusion_ops	PGNSP PGUID ));
 DATA(insert OID = 5035 (	3580	network_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4076 (	3580	bpchar_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 5036 (	3580	bpchar_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4077 (	3580	time_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 4135 (	3580	time_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5037 (	3580	time_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4078 (	3580	interval_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4134 (	3580	interval_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5041 (	3580	interval_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4079 (	3580	bit_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 4080 (	3580	varbit_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 4081 (	3580	uuid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 4131 (	3580	uuid_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5046 (	3580	uuid_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4103 (	3580	range_inclusion_ops		PGNSP PGUID ));
 DATA(insert OID = 4082 (	3580	pg_lsn_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4132 (	3580	pg_lsn_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5047 (	3580	pg_lsn_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4104 (	3580	box_inclusion_ops		PGNSP PGUID ));
 DATA(insert OID = 5000 (	4000	box_ops		PGNSP PGUID ));
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 7154e5e..c78c49f 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -4364,6 +4364,50 @@ DESCR("BRIN minmax support");
 DATA(insert OID = 3386 ( brin_minmax_union		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_union _null_ _null_ _null_ ));
 DESCR("BRIN minmax support");
 
+/* BRIN minmax multi */
+DATA(insert OID = 4001 ( brin_minmax_multi_opcinfo	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 1 0 2281 "2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_opcinfo _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+DATA(insert OID = 4002 ( brin_minmax_multi_add_value	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 4 0 16 "2281 2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_add_value _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+DATA(insert OID = 4003 ( brin_minmax_multi_consistent PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_consistent _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+DATA(insert OID = 4004 ( brin_minmax_multi_union		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_union _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+DATA(insert OID = 4011 ( brin_minmax_multi_distance_int2		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_int2 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between int2 values");
+DATA(insert OID = 4012 ( brin_minmax_multi_distance_int4		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_int4 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between int4 values");
+DATA(insert OID = 4013 ( brin_minmax_multi_distance_int8		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_int8 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between int8 values");
+DATA(insert OID = 4010 ( brin_minmax_multi_distance_float4	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_float4 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between float4 values");
+DATA(insert OID = 4007 ( brin_minmax_multi_distance_float8	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_float8 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between float8 values");
+DATA(insert OID = 4009 ( brin_minmax_multi_distance_numeric	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_numeric _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between numeric values");
+DATA(insert OID = 4129 ( brin_minmax_multi_distance_tid		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_tid _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between tid values");
+DATA(insert OID = 4130 ( brin_minmax_multi_distance_uuid		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_uuid _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between uuid values");
+DATA(insert OID = 4136 ( brin_minmax_multi_distance_time	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_time _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between time values");
+DATA(insert OID = 4137 ( brin_minmax_multi_distance_interval	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_interval _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between interval values");
+DATA(insert OID = 4138 ( brin_minmax_multi_distance_timetz	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_timetz _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between timetz values");
+DATA(insert OID = 4139 ( brin_minmax_multi_distance_pg_lsn	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_pg_lsn _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between pg_lsn values");
+DATA(insert OID = 4140 ( brin_minmax_multi_distance_macaddr	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_macaddr _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between macaddr values");
+DATA(insert OID = 4141 ( brin_minmax_multi_distance_macaddr8	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_macaddr8 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between macaddr8 values");
+DATA(insert OID = 4142 ( brin_minmax_multi_distance_inet	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_inet _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between inet values");
+DATA(insert OID = 4148 ( brin_minmax_multi_distance_abstime	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_abstime _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between abstime values");
+DATA(insert OID = 4149 ( brin_minmax_multi_distance_reltime	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_reltime _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between reltime values");
+
 /* BRIN inclusion */
 DATA(insert OID = 4105 ( brin_inclusion_opcinfo PGNSP PGUID 12 1 0 0 0 f f f f t f i s 1 0 2281 "2281" _null_ _null_ _null_ _null_ _null_ brin_inclusion_opcinfo _null_ _null_ _null_ ));
 DESCR("BRIN inclusion support");
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000..a337006
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,408 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 149999d..2c635d5 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -84,7 +84,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: brin brin_bloom gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password
+test: brin brin_bloom brin_multi gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000..263396f
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,361 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.9.5

#14Mark Dilger
hornschnorter@gmail.com
In reply to: Tomas Vondra (#13)
Re: WIP: BRIN multi-range indexes

BTW while working on the regression tests, I've noticed that brin.sql
fails to test a couple of minmax opclasses (e.g. abstime/reltime). Is
that intentional or is that something we should fix eventually?

I believe abstime/reltime are deprecated. Perhaps nobody wanted to
bother adding test coverage for deprecated classes? There was another
thread that discussed removing these types. The consensus seemed to
be in favor of removing them, though I have not seen a patch for that yet.

mark

#15Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Mark Dilger (#14)
Re: WIP: BRIN multi-range indexes

On 02/05/2018 09:27 PM, Mark Dilger wrote:

BTW while working on the regression tests, I've noticed that brin.sql
fails to test a couple of minmax opclasses (e.g. abstime/reltime). Is
that intentional or is that something we should fix eventually?

I believe abstime/reltime are deprecated. Perhaps nobody wanted to
bother adding test coverage for deprecated classes? There was another
thread that discussed removing these types. The consensus seemed to
be in favor of removing them, though I have not seen a patch for that yet.

Yeah, that's what I've been wondering about too. There's also this
comment in nabstime.h:

/*
* Although time_t generally is a long int on 64 bit systems, these two
* types must be 4 bytes, because that's what pg_type.h assumes. They
* should be yanked (long) before 2038 and be replaced by timestamp and
* interval.
*/

But then why adding BRIN opclasses at all? And if adding them, why not
to test them? We all know how long deprecation takes, particularly for
data types.

For me the question is whether to bother with adding the multi-minmax
opclasses, of course.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#16Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tomas Vondra (#15)
Re: WIP: BRIN multi-range indexes

Tomas Vondra <tomas.vondra@2ndquadrant.com> writes:

Yeah, that's what I've been wondering about too. There's also this
comment in nabstime.h:

/*
* Although time_t generally is a long int on 64 bit systems, these two
* types must be 4 bytes, because that's what pg_type.h assumes. They
* should be yanked (long) before 2038 and be replaced by timestamp and
* interval.
*/

But then why adding BRIN opclasses at all? And if adding them, why not
to test them? We all know how long deprecation takes, particularly for
data types.

There was some pretty recent chatter about removing these types; IIRC
Andres was annoyed about their lack of overflow checks.

I would definitely vote against adding any BRIN support for these types,
or indeed doing any work on them at all other than removal.

regards, tom lane

#17Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tom Lane (#16)
Re: WIP: BRIN multi-range indexes

On 02/06/2018 12:40 AM, Tom Lane wrote:

Tomas Vondra <tomas.vondra@2ndquadrant.com> writes:

Yeah, that's what I've been wondering about too. There's also this
comment in nabstime.h:

/*
* Although time_t generally is a long int on 64 bit systems, these two
* types must be 4 bytes, because that's what pg_type.h assumes. They
* should be yanked (long) before 2038 and be replaced by timestamp and
* interval.
*/

But then why adding BRIN opclasses at all? And if adding them, why not
to test them? We all know how long deprecation takes, particularly for
data types.

There was some pretty recent chatter about removing these types;
IIRC Andres was annoyed about their lack of overflow checks.

I would definitely vote against adding any BRIN support for these
types, or indeed doing any work on them at all other than removal.

Works for me. Ripping out the two opclasses from the patch is trivial.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#18Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#17)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

Attached is an updated patch series, fixing duplicate OIDs and removing
opclasses for reltime/abstime data types, as discussed.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzDownload
0002-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzapplication/gzip; name=0002-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzDownload
0003-BRIN-bloom-indexes.patch.gzapplication/gzip; name=0003-BRIN-bloom-indexes.patch.gzDownload
�d�Z0003-BRIN-bloom-indexes.patch�\ys7�����fgLZd��e�.K��h*���2�l6��A�G}�I�&�}��A�,�a��Jl�����N����/�c5���`f;�������������==<���w�'����R��~��'���������/��0pb)�S��f8�f�e�����2U/��L;b8��8�p�bp������o^e��(;}!~�xr��{1�������Ad7p��J���@����$�y�WA�sn�[�;7]h����*I��2^Z�2�b�y��Hn�3o	��(��D(����D��^��#�Y
2m��
�����e�Y��-�����%��Y"�F�~i(�Ps%~�T��(�����3W��w,<9U^����s���)0�@���Q���@U[��@2����@1W0�4�,�<���)e ���LL]x���m�k`��^���o�f�J�J&a ������K5��2�q�*!o�9��I��@�������M@Qv*fh��|��yFr��	p�FaY�p���(�3wZCa�*6HGZG,H�ZF_c�(�xf�#�
Q�-E$���OA�����t�$U�Ax�B�4����a
�7 	S��`w�XA{���x��6�B&h0@��;!�E��y��p���� �q*%nU����e8"��)�w&��[�e`����h��"���
�/@]�����0 ���)���U�J}L��90�H��kg0
�b'���q������-P�M���sm�:)LKzII	������@H�;�&`��)n����`=��X�����x90�b#i���U�[5�'��m	��D�Vh�`�h��91��>�6�KR0�`�^������k��q�- MR�:�$�f����Ic[u��������jv��&���Kb��{�)���������px$v�?M�dSi����A,WIBz�A���*�
1���I��		n���`o��[��6�e���9H/�����F��*�`~��A++S�.��x�+�Kt��p$>G1���-��� ��`L�7�J�^��1���#Hq�)C���.���@?�"��I"�W����\��^p'�M�cN�����
I�G�0��vh��^yT��P��kCk����^��8P������k�:�]p�����{��4q�����/��	�D;_F�>�f�qg3���!����<}�E����W�=���{�C�?��y6����y#���ag_��?��7o����(���/�4t��Y�����.��/_C������SH<��a��$�=����mW(�e�����[�o"��U�����
AYf�Y�}7���B�/��������%iEI�Gv��pSx����]�wSh��A��O�����t	����"�� ��nm��5�
��`�
d#����"
Tq*~"xY�M�}P�u���!a<�o��Ax�v����^�d#Bv|�d���"�4O�+I�)�u���:����c3/��g2'�p���v��c-��(?"|
��=B�p��
T�)�H�TuH�)�u����G��h�T�����j�Ty���Z�k���Zw4�F�����0��
|���f���:�kP��w�������t���M�4�7���k��w���
B��q�3tO���y�!��Qq<:�FE����i�%i7E�N�T��������W�ko,�=�Z�����a����md�h>��`S���i��o
o��5�r�;o#����)�@�4�����Z#l
�0eCD���h��q��������['p
����62X���Y=�dO���ysxk��A��fk{���
7�7���=���pcp��#���me�����W��i�%i7E�N�uT�C�=����*�I�J?�ZCH;�ga|"~�'��H6����R���2�������?����2` N�bLs����-��k��W�������`��,���-!�u�����4��-~�f3���p����3H�40����\'�:�����C(_���@G�D�������
."�<��Mw=��A��P��n�{ ic���[1�pd*�1�F,�V�
�@f�#���5;�2Y�����*�DDa���+P��y����J/���S����Ga��VT�a�2�"�� ��}P@���^��v�..*��]��K}vS����{m!������+�Q�[>s�x����-d�p���_���`�����>.)�8uSO��h:���4.2�UsJ�]a���
�����<��i&�v��������V<��n��NsA����|�w���������S�� ���e�M�d�����v3���{�[��^��1~D���H:��v�nS����vU'`��6e�)�h+�6�<`G��p�vA����an��s��J�_5_>d�y����M���P�7���5E�:���\��"�VzK|"�A�BD0�*���T}N���l:	n��FW������*�6�1��6�+����h�99���>nR�>���9����<����qWk����x��)�I���"mI���;�ck��,��F�����H�(�9
�7N�|b�A����� Y��f����(Xc>�����|��C'FGE��T`��j�l<�B'I*�
�,�"��0�-$.>���G�wN�����G��Om�w��I5;�Y�<�w�����2����6�{#�d
c9F�$���+aY=�_S�m���Z�Vm�����^8�������W������4@Ag��������fP&�/�^8�����r
�OYS�Mn?�X��[���������mCu�0�|�W>�e�7_f	�Sk����������k3�����3[�}�u��f1�J'h
�N,���2`���{0>�f���87-��������R�#�&C��/���2�SB-�-GG��ap��	j��;� �-��0�8���G8�;�R������c��B��5��-h\��u������w�oO��/H�����������1����O�������rI�^���6���3M�=j�c��`R�$���t����o��M�m������AR��i�K~�%����b�K~�U�y������)�2��`��)���D0Ol�M�D��3+R��9y�.�w�.��$���8���*%�i���K<6WxR�j����h��pq�~���������?��j4���s�����U�ztr2P�_����|r�}���>��i��U6Mq�1���������-��>�3��������\>����*.���-	>��-�y�l�\�n���'Wg$.���G���������j��=y�#���t��CQ�����C��j���e?��?�>G���]^�49����U����_]�x{����zX?ii��F���������5���{�`�w1����[<����c�;:������>�����_re�
P:z�hG�q)����t�T��Rd`���z-�g,S<���|n���
,��Bf�(�fl!M���S#��e3"���Tf~
4)�-����Fe���CB������2���N��w�S��'��(Q�2{����M���S�9H����������1��)[z����!���Q�SK�1]��V��,#1��z��fx���"��x\Vz>�4��
"��r��)��i�Q��+��
]=x<��f�:��2��X��x^�3�|��B������D���2�}��01#�	�T��4o��%����;�9����fNS!�!]�G�`�b��g�O�^��r�o*��,��rU���G���N�{k�@�D����:���%f��B����5{���C��n����U�R�a�n�+����s�4	�"��������T;��)���c�9�,RaRD���'�^.��C~�^��d�M=}��xbe��j�Yy��f(y�l�4,h�2�>��	=1>m�#����A�����-B/?��VE�����D�b�Do��U	v�W(�sr�5�����M�p:���i�����@�=w�k�U�~2�TO������0>�����$?9<���#����sGq;I����aS%.�fh�A��z��r]T$��PU����8K5��������@I�.�Dd�z"/l�M
f�un�������Y���2	������o!X�$A��/�A'$��0�y���M����[o$�����O���	�_|��cJ��a���T�Q��di<�p����^V�����Z��U5q�kQ�^{�zJ}Y�u
�[�\�A����������k=�

5��e(4�G�]�	V��+*1e0��
i�����5G}��5��V?�������g�����O���?=�����G��^��A^ F��W�����0�b�C��+Gt
��������ML����Z���Z�<��c������aF� 65�d���"��|�x^��e$P������#��*�vb��`�mv�+T��@�(�m!��
�Q��4����
�;���w���~����Z��3�����>-O���U������?�Q�+�����v�X�7��2�nci.���
�p
�u/�'�!�|�J�����=�bs^G�nC��w�>����5��z6D�m���a�C3U����Hx�����_��!8�8d�xE�-��4�����p���������|�K6�?>�d2�)���c3����M�����.��������"bX!��^����_8k�,���C}�?�C�r�h�8�~XV�v_s||%���/K�t
�v7��d����������_'�W��s�29��Q���;�z��Pb�'����!`���W��9�|�V�����k����[X*A���x.;��u��\���������e����r����M�_a`�0��#��xf��6�L���AE)~v���o���(���g�h�q��L�	��_a/�r2��H�p�w!����x%\��s���O�{4[����aT��dGE�V�0K\)���O�q���VYX�@fLe����-���yNV3?���m�99R����X]C�s��^��.Q����G���,V��y*������.X�7��_6�Ax���s����F�)�1������,W���~_�(3�Z�0&V�����"���Cn��x�-s �������H�L�WPo��8��1�O�Nh�b`�V�'z��)��eJ}�������C���m���rj7�0|���o�����w�!�H�����`����A_������O8��8������$u��q���	s%��E�"�!�[d+�{:v#�e��IR����^���#Vh)�0s����5n�)sA6�����4�O��."�������h�2e���x��t�<�uk���e�,���O��*�����g&���#�1�*�k-���6��<�v
)%O6Z�\K���-��	��
Cx����C�!`$����*XS��90���/��W�|�~!# 6Z�?�>~E;�.��K_!�4���� J��bt����*����"�zlm�?W����$
'z������B��"=!:q���Y����}�����T#@���UZz�y���N%2��{���#�?'7m�$���~tb�g�(��5A�pZ�}f1_��5�YpXB�l%3�}���|CK�����1p�������%�
2 �U/����3\�[[E�)�C��X���j�_bQ�z�aw56#���c!4�Mk�������l�;�L����eN������a�V5�+���^*.8AS�<s��La�-���o�n���Y�+��6����<�p��u�����g;���������[Yr5R����%�7��Hi�n��ne=� �3�^�>��UYc�E~�)y%�y)� ���0n�0,��+��E@���G��b����+�K��c2�$�$�����ZG�N��?�X�P��S�LT-31f���s6
�We	���O
l���f����\��Vl�.����3��0�
��]����l���\��ZL�
BX�������w���q����������U����)���L��?E�L��).K��"��Y�t���%��o��ha�
a�$�a3���/���,I��m���.`���m9�9�
�ZM�|%���g�x�	�J�1	
���F7a�������|��1�l����S������[O��>��l�Q�����6'��0*��y�~��~���#N�
!���f���)J���E�c���;� Ip���1�X��]����`���`)�)���2��3Zm����}��6+�;�lX�&�t����
V�q��G��a��*L��2��X����������_@s���h��fm $�5(nQ�\�c���@��}���6#�o?#*�Q��zs�����{d�|���!\7��R�3�5]������*��6��r��T��
�����0�������#V��8��<&��}�O?����w���o"<3�_����8���g���(�%0�>�'c$)����3+����;�N����3�1����*����O�T�d�5�I�@@�����!YX/kq�Z��ak�. ��+A���H���5�U��m��J�3���}���`i�X�y</f��x�r���k��D��xq��(�U������I/i��.����v���J�-�f"5����9�,J��9�&�����j���@��~�J%���9_�]u�
G���"�.�t�9�n��J��+#E~]�~��S��o�/�.�N"h�oU,�/��������o���:��v9�SApXPjgm�b��#���K�aBv-�aX��@�>:������*k�vr����,���mx��kb���T��B�����G�j�����
������0m���&��S�U9P��L|��0��������.�)�5��b������<�����d#������l�;�IO��M��&uZ�~��������Eopk�����A�/��f�)���A;���0B���i	��XW�'�L�����eO�������t���i�������y���E	aE��=/o�pX���A�8B[����c����<��T0p�/�<�i%qq]�	[�����x��(��qL���,�B�l6�i0���k���&��'�
�RY?��������a����g|]����Oe��.��s���|V�JLv���i2�{�vV�td=^�5���LG���!��pj��!��S#:���������p�����^�EG��f	����}�0\:�D�J5x������.e�l��6
����J���m�1�;��'��v�;I�/L�2T��2>A?#��y|B�,Y��t��6o"�6b��xHh�_�Q'G/:t}gZ�Vqzx���x�������i��������
��_M���l	l"m�>�a8�d��Wx(� ��R��Y=	��5��vN^6b�{;�C�AP~�E<S��,��2v���b��"�	�WKy+�ip�����v(�����It�&b����[;N^�rp���T���l 4�������L*�;!o}��?0��e�4����&���s�!���G�ox��?��c�o]�1�"�-��(oM��88�U��eN*8��J�Np:�(S��j����+<��F(�5�(aIy9���|�L���
����?R�������@�o\V5�m� ����A�]�%{�,\s_:+���4� �J��P�DLt*@�_1M���-�!�gV��&��''�X����Hz��B����$�
�!>[N�J�vgI�l>�A���A�{�����h�|ug��j6��W��8��'j��������t*�����-v�ZG�D�\�b��F���}���aA��L1�\��V����`�_t�yk-T�/�O����r�f�H>�5�
�u1�N{LW���	o���<$����f�:������a�Y����x������������T���?/9������������]M�0�%v,#P~W���:��(�2����X{q�i���\s�5�	�#��������v9�����"��.q	E�r���)��������T���T-���*�)��IW�_��[��;�ngT&�,�� KE�e���-wXej�8���0� 7Y�\+_r��e����4�f����0��K�{������Q������6��V�g9.{�)A���Z�@(	lw~�� ��*C����m��tFL!A��1S�w��e@���]����D3������"���?�Gg����%�H)���&���G�[��"��v��-�w6~��o���G,��0F�o�<<=�G��������v����7H*@�7��^�\D�<�������t>r�]�gx��K��.����?lUh�6[�Z���Kgb7����}g���<(..e�#��������E�`�r�%o�0�CNXL�]�2��������>�e7��|X�p,|� �X�����>A\y�������7,-�{^�H�?�[;[$����*��n�Sa�9nTL���x�nd4��vv����jO���]��A�v�O��a�H;\��[`�8�-a�N��q����f�t,C���69���#�7X`]���a���f�^���V8��1,|?\��T��6���+��r0�����U�0�o���d?�HI�X_q����Gx�U� �5���M�K��u#����
m�|sr��mM�(Y���R.+������q����>g�6���6J��.������&Xz�A�g�Q���B���|N��bqAJ�x�d���a�;��tR]cg . 4)�\�����2avNV��H���z������ij�����Cv�<p��h����9��9AJb�K�����s��28����C��xc�S�Z���SD��L�w���/����i�_��+<B~�U[+~�6��|�v��T$�7N9���
�Y�-���<c��`r����1����b9K@�|�V�E�:G�|SM�����T�{�������}XMd�V�{�%[�����~���g*���a!�\qSF��*@���D#&�'���a�
~E�?��B��*N�i��R]�P~*]����Z*l-q�u��r/�^l�Nf;lE�F~C}Cmla7�{O����QM�x���S�J����'5�$�{��I��N�i����P����@�t���^�����n�W���v'�w��TZ+���y�PA���HB���$j����8�vE��{��#;�M����l1X�DP����������>a�b��	Hu�������Mi��<���k���_@�U���S����c#8N������;$�^���U�������Y��-o���`.GSI8����]Xc�2�o�c.�b�����e��~f�3y����6��.�T��Ym��
����S�Q�
������m]x����'��	k1ZX�����������$�
�`Bf�u6����
�w�o��%����5��o�Xz����_G���v�Q~���9���zwM��������]��7��SP�����Fq����	�M2��,]��@HI���^�E�U}IHEU�>�����DV� R*��
�2���6@���&������,���<JbE'�q<�K1{�9P�s�PJ}����H7����!%)�	��l����d �)��XI�.cK2u~���Y�n�R�� 2��m�
I����
��7�C���	�o7��W��.���K�[%\�<h�|sjID��.�,n�+)H�]��������w]�	��P���Z�7�#�k�5<�*j��>�h����%Zk|�X8�~��6R��d��D���Z�lm�H�f��I�������*��`������el��Gb��>� 
q�!�\��\�(�t���8��a���FI:�F����jZ�u��z I���BXu��La��U�[���t�FW
<4��#	��������+5IJG�m��:�o�[H�r���;pH��W3i����G�����Q�p����4�q-.q��;q�����������^���#	]���I���4�t$����=��������l�a��N��@8�r��&MK�B�KF�R�_)�������KX��������l��Pb	K���Wf.I�f���oR��
��Z���^4�T 5����	�F{��r�����bT�����.i�R��Y�����S���eT���JS{�
"%�a�R*����	��k ���H����:s����Lf�;:�����u���F����AG��"��.f�1Eq��t�(Z}6�M����
����d�Z
�5��+�iMH��1��m��� s-FKOb��U��7�/�^�X�����zZx��kJ�-<����t����7�0�U��G\6�z��z4�[X���)���5�%�Q6&�}�^&��8�M�\�y�~����]m,J1��4a��������������$�8�/,������<Kr����a��_���3����6F^*��&^&�37/x��`��
��VMc�������Ij���������)�gxx)u=���w=D���f�
����r�[r��7>�t�g�5�Dt<���8�����8�*'��/���9�,���/7��I����xb���H��tic0n��Ig����bM+Y��?&w<���J�����/�r�7�R/x����K^��3�2����L�\�y��\]%�[p~����uSA��w#��(��)�����kf�9�Y}o�<��*���E�k����
�F2�2��X!4�r����f��r1��h�}�+-����j�c�y�G��{��]�i�K^��-�T�5����^��-�\��>�I���uK�!t�C����r�u����4f/"��7�������Q7��0��N�5�������oE�6j��K^�6M�L�5j����W��W�i5s��g��}�����w�1kuA%�"�����c��B{�i�f
���S0�P1J���eow7�{���j��4Z*8�
�q?�����"��������[�:`��Y3`��y3`��]�|�M:�yD����b�m
�oLp��`���S�1��,���B�y#�E������������	�V�|4c����w@3&��(@����L��#��t8���������������h%5�h%5�h%5�hEr���VxO��I:����s*|��B�APU:��*p�Y�\h��;t�rw�`�&�����&���(\��(�7E������D�2N�
s5'V�	'V�	'V�	�2s���1�8�8�8�g'\��Dh�OE
0<�4��d��^3��s�K��:����u@/=���^ �K�I�
���^�4��R���2
���@��t@'���N���:Q�|���N�-��%���X[��.$�� ���8�X�J���"�S6�V��v��w�	�8���-���-���-���MX�����SM<M+�����:��������%u���AI�i�6m����CS��<I;tr6�l@iaJ��L��0a�s��u{�p#�=����� V���XIj"`%,ZI:�����`7K[Z�z�aCz�aCz�a�z�����(.�GP\f�"���E@q,S\���"��������f��fv����y��S��46��=�4`a\t��Y�	�l/^6�^�
Z�lq��&��u��a��m)�#��H�$����S	�nZ�y�`"&l���al���us�9���j���6���j�������6�r��6�4���
���
�����r�u�c
(�6��Rkc
(�6��R
XPj�����������v4��&�����9�n��f�������\��
��&�/���J�������6l9&l�rX��$J��(������]ctv��I�5F'`���f6�N�6�N�6�N�6�N�b,�*�5vSAP����	��&l�{��n����d���96�96�96�9���A����r=C-lj��&l����a�j�6��k�B�
�3�Pgb(���P@�,WgmC7��
W-��j��&lmi�3��t{=O���3��"Z�VL�����a[1a=��
MV� �QD�>j��Gm�0`Y�����`#46���)�36�36�3���`��9�����
��#(�k� �0��AW��7k"�.���!�����d�&�Us���b��z:��_��MS
fb�����
��	�6���/sC��cJ�C�Pz��b����I�8���F���%��Bb���N'�iX�M��^�2��yR���.�e4�\�M	6�)0��$�:)5o��;���^������.�����bWp;������_�>zA2L��~��K��*����X�����:�*��ixny?5��<���*h�ka����k��F��m�	����e���at���(HL�\@p���D�	�p)(���v� ��L8g�"`z~����	�i	1��;�	��U$L �KA��D��DL�!&�&b`�Y|�&&R`"
1�50�js�A�%�L@�0�{�@&���p�H�0���v	��C��P���B�/�`{X�Oqv�������`���o���6s0b���G����of'Fv�v��4�#;~c�{�!~�n1��x	-�i�	�H�o�|�/D�o�RB���� ��Q�"tt&75ah�������^!h��"�)��A����!�O����90����E����<]�����rl
�o��Z�����([�:������f�W������	�G��Td\�@	=�&��<c�/�`��B;a�`_���T���OH@��_g��\	RA��?5��g|����S���3�����	���o^�el�WUI�+-QeL������-)^���OHR��l�x]t�>`vl�h�������d�GH(a��F�kg!	���*��AP��H�T�|�{v!<4��=���eA)$OQ��j���G:~�!�<�	2� j<
+/�P�����|�4��'@(	d-��_-�.A�&���7��e������*w���7��o�	�V��t�4�������_VM+��������W��,n&��p�C��
�E�eY/���E?���pH*
%	��=��]D����v���Jm�]��Qm��$f�S(�a���L��W�?���%#I�M�c��AP����2��Hl�Dw���/��Z	�������_�z�����z�$/i�FcS6�r���_&��U��&����M�s��9Xt�s�6g@�1X\�_��o~��2���;�{�Q�3�l<����������aS��OBZrS��/��&��|����%
?�@��	�,��*����6'�,f<��Pj|l<�V5�"�<�6fA�v��������K:�����`�5�A�v0#�����iH�� �I�_
]�����Lh���?9��%�(z����Z��y��BE����A
����|z�E_C]'j���q��nR��<�����yy����s���#�������<$@�g�O�q�Vm��gP=�j���HM3��RCN1��.��]��-�R��~Gx�$t����d����.��d*n 6F���;�n������1��}Z�����>�l����#}����D�{D}sD��s�)s�qC]Z���#�#TO���,���G^��cs��Z�l��VtP���x�������z;t��E�@S1~;����!��N�3��d�'"
Y&�Z���5�[y4���>@�|�^�mID�V�eY-�.��EYUO����hY��*i������A�c5/'������XR�s�����r�-�^�+��q��)�D[S��,/Qjk�,����'�O�������`�C!m���������/|�n_�@���_�RG��}��I�����N�w|��_����+��i�����^���/������x���������<�3�.[.�eK��h�@�/|f�=~#����P���A`u���>+% pwI�����OuhT��Bt�^�P�D^�CE���-�����4��,|�d�|{t�%>�szY@�{?����������f�_>?�������]-��W��C��{{��������
��J�����E�,��=x~5�;T���3d����G��q������R�v�����	�w������l�(�Ob����{{���S�h��r�����zT�E������^�������P�-g���6�e�d+�v��t7{�d[�r���T��g�`�ec��|'����M�f+���h/��W.V��:_��wsR�i0oV��d'��$t�J��|/�1���]�����j��@�����?
0��<�}ry7wU[|@�m't	����������K(�4�Bx%p]�.WH����S�es��v;i�
�R�+��6����X7�G�����(P�E�l��
�������T�7R�x�E@gS����D���zyz�|�CLNN_����#l�'���WG���R�EW���b�l[�����B�*�����r�xv����r�!|�V��������u�,��_^�]��`�?���I��{����I�F����t���_8���=���=��gG�_���6��F�*z0`V�����E�fC�&�]�V�����1������Ua7��1�����)l>UtaI���_T�aW��������+��7�|�F��"b����w=11B�%��o������!;]�O��3G<M�����$������[m��o��Oq���]������K��G�b<�����=������Al=Y�_e{�^�C������3\��������E�l���/��������1�e�ooq������~�����D�������`"1�Lx[��G��O-�0��h��1�}���1���I�����������ob\�K�%�;�����>�����,�����pE/���+��Z��S5A����5��i��'T��fP:��D��&iL3
�!I
���)�"�?��Q��9I���0N��Z��_-�+�uz�:�,�
�:�#�;1��b��H
o���%�����Q� ��bt��(�I2���^�oBp���&��Dy�9 �&T�,��WET�}+~tP$7�Y���l��T���%c(���;HG���Q�5�@W\��aI�w��4���n���n���t�Q��8X����u�y�$����E�����< �"=I�C���?r|r�r�4��Rs6���vh���$�����	����Cm�����C
$�k���D�9<F|����r2���o������f4_�$��>�N��p������A	��~qtLxyrJ4��y"��h�^b@���7s�T��`hE������c���;�!�::����U�������.�M�o�m�(���0YN�3���%Y��H�@�L��<�1������L�����<zE����CZ��Aq2��V����a���f�~:�����;~A�^>��Y�f8l�%�j2�����bE��I9+.���*�F��F��3�����������B��y1�JD�*�S�����k���������������;6v���6���R�������!��wP9�\w-T�������G�#�������p����hK)Q�r�b#Qeo�z��9��H��rzptvH�=8=����x2&���\�K�����Rdld~
���.�_'-&(H���[JA�9x�$I�'0�����"��lB�te7�%�V�^���tuX��H*�,�j5]V��?�
�bZ����mieV��D�f�`����&z����g�K������y[	l���[��3M���,F�������(x�J
t�v�%����N���I\L��a�#�����>0c	���j��B�gm�lq�o��Vk|�
h�q��S������9���1�e����#��#�����{S_.���+�[Q�b��u4Z����n�L��������f5sP��HNy�3�(���L��W������"���>t�2K��mJ�|���������7)�w(�;�99y������x��u����O%��ho;y������&����Gq<i�d1�
+�7�_����g�����wTB����-���hZB4�z����/`��t��������Z�&�,�'6����_�W�F�)*��=���&E %�Y�n(�uu�� �x[��@�b�?!*dFyxzzr�G�C�!�Td�U�:���;.Jx��yaK]�^o������?��a��H����\t�����/2�����Qo<CvbC����&%����v9��e���@�(�?jV�)��v��tun��qu�vz5�����U����t�uH�Z�����f���>&����j��/3�}5�n
V�X����6s^/)gt�^����2��b��jO>����"�i���zk,�d��9����k�C�p�g�������-������c���	��B����������c�:��%^���5(���y�)!��#�tQ�;������3v?�T���f��*X1��j���������f����%�l��%��7��X�� =4���v��	����k�>�f��]	Gqi:V����x��Y9�#EES����E9e� %*dT��z�\��C.����eX�����t}��D_Y�����
+����&���M����]�":,��i�+<��84^�b�+\4\�G���������=�������p�yw]�~���YE`�����U��x9Qxn??9;?�Dt2��\V����T�+]�	��_�~G(�c������^������������9�|����;���*G(*f���E�������p*"q�#�.�����[.�7�b�;�Lid�����/��9�5���oC�=&���#2&�8~����Q��;H����/�"�tj������e�_|�L�a��C�g����2�g}q���<�pb� �i���J������+��!
)Px����R!Pa�{M��3��$��� ���g�z��`�'�0B�^$yY�st���%
������X�����+��rJ�?yK�.V<��.:���X��u�KX������U��6��:���R5�<�`�0=n����+���+��0���x�����n�
�?����x&C4��@��J?�������c�Mf���y�z9���t2�H����������#i�r���n!�l��+a�1���
���Ku�nT���
;y'�!|[�y
U�pZ\P�t�_;o'4��'#X���l�������P�RcamU����g���\Vd������o���/���8Vg��$�������i`��I��F�l������4��&�M�3l0t}>�hS`LmxY>��`#��-F��d3�/F`�J������JM@����=3�&9�8�O~�������:���HFQ��w�q
�j:������A��3���P�O��m���B�����S����P�O���D��9���8�z��4��i������Q�O���F�?�*tqu�����Q�����G�?�z��<��y������Q���Q?�D�����3��9[������J�L���D�gJ��S�����|Z���#Nh~���8����c��c���������or(�w>�����8'�D�����?����r��0z���`7���JR��g`
0004-BRIN-multi-range-minmax-indexes.patch.gzapplication/gzip; name=0004-BRIN-multi-range-minmax-indexes.patch.gzDownload
#19Andres Freund
andres@anarazel.de
In reply to: Tomas Vondra (#18)
Re: WIP: BRIN multi-range indexes

Hi,

On 2018-02-25 01:30:47 +0100, Tomas Vondra wrote:

Note: Currently, this only works with float8-based data types.
Supporting additional data types is not a big issue, but will
require extending the opclass with "subtract" operator (used to
compute distance between values when merging ranges).

Based on Tom's past stances I'm a bit doubtful he'd be happy with such a
restriction. Note that something similar-ish also has come up in
0a459cec96.

I kinda wonder if there's any way to not have two similar but not equal
types of logic here?

That problem is
resolved here by adding the ability for btree operator classes to provide
an "in_range" support function that defines how to add or subtract the
RANGE offset value. Factoring it this way also allows the operator class
to avoid overflow problems near the ends of the datatype's range, if it
wishes to expend effort on that. (In the committed patch, the integer
opclasses handle that issue, but it did not seem worth the trouble to
avoid overflow failures for datetime types.)

- Andres

#20Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#19)
Re: WIP: BRIN multi-range indexes

Andres Freund <andres@anarazel.de> writes:

On 2018-02-25 01:30:47 +0100, Tomas Vondra wrote:

Note: Currently, this only works with float8-based data types.
Supporting additional data types is not a big issue, but will
require extending the opclass with "subtract" operator (used to
compute distance between values when merging ranges).

Based on Tom's past stances I'm a bit doubtful he'd be happy with such a
restriction. Note that something similar-ish also has come up in
0a459cec96.

I kinda wonder if there's any way to not have two similar but not equal
types of logic here?

Hm. I wonder what the patch intends to do with subtraction overflow,
or infinities, or NaNs. Just as with the RANGE patch, it does not
seem to me that failure is really an acceptable option. Indexes are
supposed to be able to index whatever the column datatype can store.

regards, tom lane

#21Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tom Lane (#20)
Re: WIP: BRIN multi-range indexes

On 03/02/2018 05:08 AM, Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

On 2018-02-25 01:30:47 +0100, Tomas Vondra wrote:

Note: Currently, this only works with float8-based data types.
Supporting additional data types is not a big issue, but will
require extending the opclass with "subtract" operator (used to
compute distance between values when merging ranges).

Based on Tom's past stances I'm a bit doubtful he'd be happy with such a
restriction. Note that something similar-ish also has come up in
0a459cec96.

That restriction was lifted quite a long time ago, so now both index
types support pretty much the same data types as the original BRIN (with
the reltime/abstime exception, discussed in this thread earlier).

I kinda wonder if there's any way to not have two similar but not
equal types of logic here?

Hm. I wonder what the patch intends to do with subtraction overflow,
or infinities, or NaNs. Just as with the RANGE patch, it does not
seem to me that failure is really an acceptable option. Indexes are
supposed to be able to index whatever the column datatype can store.

I admit that's something I haven't thought about very much. I'll look
into that, of course, but the indexes are only using the deltas to pick
which ranges to merge, so I think in the worst case it may results in
sub-optimal index. But let me check what the RANGE patch did.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#22Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tom Lane (#20)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

Attached is a patch version fixing breakage due to pg_proc changes
commited in fd1a421fe661.

On 03/02/2018 05:08 AM, Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

On 2018-02-25 01:30:47 +0100, Tomas Vondra wrote:

Note: Currently, this only works with float8-based data types.
Supporting additional data types is not a big issue, but will
require extending the opclass with "subtract" operator (used to
compute distance between values when merging ranges).

Based on Tom's past stances I'm a bit doubtful he'd be happy with
such a restriction. Note that something similar-ish also has come
up in 0a459cec96.

I kinda wonder if there's any way to not have two similar but not
equal types of logic here?

I don't think it's very similar to what 0a459cec96 is doing. It's true
both deal with ranges of values, but that's about it - I don't see how
this patch could reuse some bits from 0a459cec96.

To elaborate, 0a459cec96 only really needs to know "does this value fall
into this range" while this patch needs to compare ranges by length.
That is, given a bunch of ranges (summary of values for a section of a
table), it needs to decide which ranges to merge - and it picks the
ranges with the smallest gap.

So for example with ranges [1,10], [15,20], [30,200], [250,300] it would
merge [1,10] and [15,20] because the gap between them is only 5, which
is shorter than the other gaps. This is used when the summary for a
range of pages gets "full" (the patch only keeps up to 32 ranges or so).

Not sure how I could reuse 0a459cec96 to do this.

Hm. I wonder what the patch intends to do with subtraction overflow,
or infinities, or NaNs. Just as with the RANGE patch, it does not
seem to me that failure is really an acceptable option. Indexes are
supposed to be able to index whatever the column datatype can store.

I've been thinking about this after looking at 0a459cec96, and I don't
think this patch has the same issues. One reason is that just like the
original minmax opclass, it does not really mess with the data it
stores. It only does min/max on the values, and stores that, so if there
was NaN or Infinity, it will index NaN or Infinity.

The subtraction is used only to decide which ranges to merge first, and
if the subtraction returns Infinity/NaN the ranges will be considered
very distant and merged last. Which is pretty much the desired behavior,
because it means -Infinity, Infinity and NaN will be keps as individual
"points" as long as possible.

Perhaps there is some other danger/thinko here, that I don't see?

The one overflow issue I found in the patch is that the numeric
"distance" function does this:

d = DirectFunctionCall2(numeric_sub, a2, a1); /* a2 - a1 */

PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));

which can overflow, of course. But that is not fatal - the index may get
inefficient due to non-optimal merging of ranges, but it will still
return correct results. But I think this can be easily improved by
passing not only the two values, but also minimum and maximum, and use
that to normalize the values to [0,1].

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzDownload
0002-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzapplication/gzip; name=0002-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzDownload
0003-BRIN-bloom-indexes.patch.gzapplication/gzip; name=0003-BRIN-bloom-indexes.patch.gzDownload
0004-BRIN-multi-range-minmax-indexes.patch.gzapplication/gzip; name=0004-BRIN-multi-range-minmax-indexes.patch.gzDownload
��/�Z0004-BRIN-multi-range-minmax-indexes.patch�<kw��r��_���F���l���'q��Y'i����i�DB�I�K������wf�!Q���K��$�H����},�N��w����������O��ho���s0���R��JDl0f��K���������d_d��M�n��I�O����G��E�t�����x��x�f{���`p��{/G�/�G������t�7�$/��Og_���F����X����p3�/�=�BW�U��6I=?�x�m���s����o���}6�1s��!��c�F.���y=�/��p������H	�M���!��.K�@��6�(������S'���7i���Buy���X(�x
�����T��7��$��1{w�������������W����� ����m�mI�P��H����W0��)N��`�{0����
��.p��r��z��M,��/��X
�`�a�oH��"��
���2���2�/��b�B\��;7l�#�&TqJ
B�����[�����"�6��@;\:�
$�l�aS_���3�
hJDO��J�H�	��]�C����9�M�<�T�A������c�G�������������	.'���"�	X#E4Y�E)P��@fB$T$wdC/O�I ����hv��N��\��T�g����^��O����th�Z-�c&@]���G(E�{��FL=�MyE|h����vM��:0l���������SW�@��/g�hv�u����G��qQ,���8���q�U�����{`����(
�Q�FU`�G��Q	��^,f1�W�G���54&����Q��!+��42�1�����p�"S������a��8<�����CQQ���L�`�`^�D��-�AG������f�9�@�H�?`�q��j�����q�W�_���t�:��&��F�&k��|�h<���v�#������j�f������3�;h�����u]3��f0^����X������?�N'������D��*��\��l�n�H;��GO�=�����O�s�����?���������\F����U�����/e��\�aD p$0l�Z�����qx8o������^�#�V�O��+S��X8���>���{�mh�A��������u������l��^H|����	��q�
����l��)4C��:�������V!
x6�����w �����f�#�|p�|
���g��������Ov�v�#�|H�h�=>?�5���G�t�!5y"�q�f�Fd����!��U�L���?"w��>��0d3o�V�������#A6�D�B2|*{iw�Q)����x<>�����gr9����_���7�O��������T(Sr=+�h�1�o��W�
��yx� l��U18��les�;����Q��<C�dp��G�?��>���;��c�>M����G�Z��������U< 	�UTK��yrP�������t5-��,�i��4���Q{��Z�G��^!-\1e*�S'aob/�9�T��<�=����W������a�:������,��^S�p��:�;iN��4?���_�^"0S/�K#�a�������B��bs�0H����p:����[^L����2��$���5�81��l"�	�'�/H��iM��"|�%�rhW�����/��[B]��8
�^0%��Az����&3g����,c5^�k�y
e:���� C��2R��T|B.�W"�a�9%F 4� �6���"��E��T�2�����B�(��E�|�<La�C���6eE�)�
�2k���W%(���W��K	�*
�wI	+	�wJ	�+2�{�-	�G����#��!�����=n�Q�}+@1X�%�bT��$����5"P�6
"_@?#2�^Y���+��J$�(*�d��z!�h�.F"���`D�6]��+~�Q�`q��L5��w�(��
��v�I��3���R�*fO!�:dk��TL���J��B�a��
���Q�P6�j��E��6�sT����QDdUZ7���Ao�2,�S���]�H�X�\��}�~'�1���,�;X��1��x�N������+B��_��,@��kq�g4I����Jv?��D������$^R���IS�9�!�(��^�KM�����	���h�`Q�:��i��\pw}�������G�]�.�T'&XY7�]i��A��$�h��i���9��^�/a��f��<�e�6���n/����4��lo��
Q:��&j�m����~�X
��X�
X�������_>��)�b2��'a����v��u��Y*_(�'~�<k�yu���;�p�����s��fsni�5"�M3���x��������\��]+�}���`
b�<L\vf�18	�/��x���=�&����D�����C������~=��SGX���f�^.��X4=�������{4w����-�n��B7v���>�=�`VF�x�2����n�g��-���4���4����r�}w?���+A����\�m)����G�)I����R��_���O���>y�m^�pxX�h�u�%�����L*���a\n�:@��6�x�&
n'4U%��yZU
�;*�.�3����/w2<��`�\q�S�� ?Us�(��}��6U��~]o�v�h�s�}�va�}M����d4�w�f/��(��I��q�VF�����p�lptt���6�P`�����~&�d����e�����Dm�4j��b����~
�[�k!.�7oAP���-��	3-�����{�*Q�l������,0A�'������9N�m�u�Msh0�(U)e������$p�������fKz�A�kv	�=�����s]�g�d_A��m���6�D�Ry����A�j�P��W{�%�d�Q�Ij	�;}��6�e7s4=����.��[��d{��L�=�z�	�;�H0q�1M���'�{)7b����������g��:+� (C�s�V���C�-�=M@�T��	b�fW�,!~6��(lE�(��_fX�����W�yF��o.nF�T�A6��6X~u-'��K����H�~�lmS��!>�w�+{�Ig��kd�S� ��KH�)jda������X����C�B@������4Cl�����M���D�e�zG;"�49P���4v0b
%���py��k����/��_5�����#�5��� ��hP[�� 
���=}&L�6���E�v��a��l<�b��<�xe���kyQ�l����	v	�*kLV`QXM�����6�F�H�������3��D*����M
pc���V���C�
1������uHKPt,��}7���`{U�O�N�����{���� 
A��O*��!�
�H���H���?��%���r}�01��K�����6�\������V��n�W��h@�+^Y-��F�;6��Y���T�!ySP���Q�6�5D����R�0�����������f<�;� �:B����;�2	�5��,�*�]�����������H�����JDd�-��c��M����"n����IP����������_.�_�=�r���{��5�����F�;�dTw��yT�`A��j\i2���x���+�iD��u#�]�p�WK=
�1![��or�ZS 7����!&�l�}M�
��k�+|����"������V�18��6]�Y��e^��b�'�FBL�}i���]j�h{��d����x��LC���L�#�!���DG�S����j�V;0*�NaX�c<���M,�����Wh���t/Y��2��[bH�r��(�O���#�>r-x�O�?�����U�6��v3�Y��d%C�%e f�����/g��a<��^wu�d�2W��o�c����DN@�Bc����T���c������/��i\"���Rh��md���-�l�j��������X�[KG]+�[�]o�����j��C��q[[}� ��c0���z�MT����s+R�/�^�������0��/�v�KQ ����X/���V�1�+��[#hg����P MB��_}�~h*uL���V�hJ��a�H)�n>�"N'gl�H�RS�hMw��9t,j�����L�G�b��@�I�[}6$�t�	�Pq�B�X#�Z��w�{�F��f���j�BQ��L���U���)�c���@_�@zi�����Z/4���\������1�0�h��0A�`���4/E}�,�7��i����2+���"�vF�F>_��lf04���������G���=c����G��;�&�0-��������R�j�C��:7>���C3FK��*��:���'}C�([�oE)$?���Gyn������NINq�Z�w��80�!E�8r%�m��~�J!�'�F8Kw�l��d����h�86E�l�7�O�VC��eR�x��e6�LD�&��_��~%�\���7h+�5 �(��9�%�M��*�M�����bNVQ����c�CLQM@F);n�I��u��)X��6M�M�eHDlu���|��)3q#$�a8[��_��r���7��_�}�|������7�����:.���aB�����@���s��U��d�+i�@X����9���N��8C�q������W�K��r����(�+a��Z�{���P
PS��F�Vg�6����UdUF�5\If"Sg�a�O�D�
k��5�>�<�_'�Xw���������hg��>>$n��$b(�z�U��]�k���
s��Y��:(��<	e����DW�	l��������gM${����:�hbNQu(�� ]Cg���G�n�4`&�,0m`��;��;�8m��3�p�1����%:<0�J �2���$c3Zg8Yh��)M�;���V������$�9�\c�L���(t�M/S�Tp��������"���$+DnP�&8��x`<3E��G�����/���NfQ�K�?�BE�9l����-Im��B�������NW@#dmk�;��J$rj��m#.�S��j��3jU�"9��\L���p�h�kF,���,S���4�XF�B���,i������l�����4aPf��b�+�q��H����f�6�M�y�m�,����l�-��������M���j��6��m��������s%��A#�cc�A�w�$�<�9&Q<��K�d��	/��5��+��^��>rv���m6��q��.�tg��oN�hJ�r��y[�<��� ��w����4rB9E�&����Z�o�?�@�/�-@��r~�#�B�JG���Zv�x^��5&���<�)���XP����)e=�iV���pw��/�&\�n��1�x�U��
S�����Nq.
HW�m.�I��;��d�P���
*<�"���L�2%���+���)��������3�����G���c'vA��j5��������W�	a���ZQ5�����&��_	V���pr�4�w�P\:8���U�X�l�j�)RO�tf
�;&��V(�'m��'I[��k�VWg��e�ff�^�$V�"���������U�]V�v��.��Y1�,�zI����nO���I�w0�4�2������4r�,Z�s�+�>e����]��6R��G��i/��
�)+[���~���2k�@B��D��y-o5|�Ft�+�I}SOZ��Q�����mbeV��
| WD{Q ��L4U h�'��M���m���\Rf�
W�:hm��D� �f?-�@�$�ZZiM ��BJ
�F�Q�k�z�ueJ�t[(�1�%4RI��)�lA��� ���x��+,�
�O�}������
�������5�m����8H;S��������"L�B
��v��i�1��������H�3�+&��,��)�^J����.I������!	`�������U����y�Td'��)`�]�]]]]�ni�	�J>��r�%���,�EsK�J����o#��Rx��o����?��$]S_C,���� �9���,��w�����@1pS���{xX��&=��;Ek��#����q2:������'�$�,2��'�')D"`�'�Wy*.��
�F �p-fC�49^�f���#�.l"e�%��d	1'p�������s�"��')X/iy�>�&�
���akF���_ M�{����}~���&o���|���&�������Jg���y~��/
m�"6�	�V~�Z@J�|�YKb��VS����������j��7RD%���gO�U�l�������e|�|17bBu��I��5�?o��}��A2���1�x�A�a7#pEo$�)��)���o���%���������3����@�=���� �`��������a�2Y]���c��B��C9l�v�E���|5a�|h�b���lq�����6q^�V��2�
#U�����x-�Sz��'�����>z���sy��g��G���
x��e8�)��Mb< �����,��M�$2�^��T,�D*i���XL�-��7��eD/��R����.&�#�������L��X����.�E��0yR���J4�p,���r�;���F���@Gr�B?��(f`������u����K)Z4.d>06��Zp���F�v��L��%��-���#:f����K"���A�3������,�`�����-r1]�L��-p��-�G�Yr?8���x*vDs�B��w���O��sd�3���P!���n��� ����8�w������o�6��_���N:p�O/T"	�`h)_	�{�u�����
����k�,�tm�Z��������w���	G"��K���&y\'<
�)~��<YNB�
�m�p�I�R(��(�A����^��A��^��~ta��:^����p�M^h�y�s2��?����b���~�QRI�8�~�NFub"'�����>��iz�Ym�d�\.�Q��*
4���xE��c����1eGOQ]�^X��Q��+���\�G���k	���&���IB����,.�IZ���kpM�_7�C���2�D���1�G��K�����1��`��f�^�1��&$h�j�	gs���c�m
�����o����
5,��eLH�q�'��.An��Q�GS��+5��������[���I����"c����r���d�*�JeE��)c�b	�N-�6��@[�w;�8��D��PBYg&���U���UOPr#<X2�&a�}����[B���tR�����Y_�����0M���66D>��1rV��c�d�4�Gr��cAN�c��.��q>�t1*c�QB!�K&���J�o�6$�H]�q�i"7�$x$;M�s������M�)!�x�s��L���W�����f��;I}��IfZ���~.[J����Z���-�G���d�I�#s$�����uaj�|6�63p�AR��w ��8�m#��+
�����6W����{*#�[�������%��Y0��!Qu��M#�u)��y���J���1h*)���bE}�p� ��?
u���\��k}�����#J����0
�#4��]�`�@.6+�(���� fP�X�R��T��;�b||���`�EK
$8.Ii7	�5W�<�����<�qe�ey���Y*1�_9�z�Vz�
������r�o���������;��mp��lX�E�I��i�7W����s�,0C	���
~�U�`��'T:.��Y��kB�!�Fui�$yHE���*��/t�""�v����������s������dms"���F;�	%W����s�����L�iS��}e�5bgY!��fq>���J��R�!�{YR�����I����$��\?:9�E
��0�h��~�����I���l���6MT!D�.X�4��Dx�Hq����H�#��R�$;GL�����9�:	��p�����;����C�&��I��"����c�2S�r��m���A�u�
�Mv�l
�����a�+��{���b�>��w%�L�(�` ���\��L�h��R���c`����hj��qj�w�����Z���LMu���}J��Sz�"p174R����I��J{��X[�<!pz��SY� c\��\%P�u�H�UK��
(Q�|!u+��:Bb�Q���BEU��J�8I'0qY(���������@p���>�qb#�|��0@����:��+W<$8r�D��J0����R��B���T)�~'Cz�y<���rm��o�o������uK�Ne�)�O���3���~Ueb:�k;8�)���y��7�M��)�"���M����������u���aURzs��!�p8%���L�L��D`&��d��������_)UH���v���c���#��Hp��Vk���W�Dg�P�<�92�wY��(B����+�� �<ZS�+?�(��C>x+���j�%��S~�P��B���n���w'�����'�*}���>s�u��D���I�+��C�E���������f�Hy��W�v��-,�5e ��
�"D\�Xw,G?�n��3�y$k&����x�\����bQ����j�U��G&
�^z����I'�M/��������������_��������3����n�ba�n,�A��rE2t.��#,���2�|O]ta����)`\U�k^�-���~d��5�m"���YY����1O�%��1A���Q����84��N�K��<�AO����M; �[�c{*P��*�3�4:�5S����i�:��������Y��r��Te����Ru0_�����lIG!�Q��"}���"G�$�XU���	�J6���.�5�v�\
�hr��3�$�|*��=5��b�V6����<�V�FL�U���U�K����=�����-��&���.�5�"�Y�-��o�3��CeT�c�o��j���W*H�,,��!
��mR��7O(
�����$�L'��J���YP�h���##��69e��x,2Oho����(����W=	�P,�%�gW���8�DG]P&-��5,���������My����z��b�M��Zf���R���
���'�����E���`tK�����Qv����R"(X�\B��&T�*`WF�XD%���o
;T��Iq��sB���B���1�����$�ge��m�I��J�2$h�|���B��"�`�����):����/c^Y��w���X����o>������c����G$
�d
���y�'y�>vk���������7P����M$0������O%�8�����d��7'�l���n4xb7P�[G�:]�	b%-�T�In�q:��K���%��I��H^��i9��M�8r�$A�S>�w��~u�6� �cL7q��b�7� ���:�9��4��D-��S[�W�}���������*+Nx�3_�Q��[C��Q�)��*�F��U�\�q��&��.�Q��D�6q�^>#d��o�f�^V>Vsy���G�T��$k���*���t��O^Au���2��DTg�����VY����AU���)����K�h����~!^@e��)�K-��{k�M.b�����rb����"(�.��?}��%x�Ow�������`��Y�dwB����=r���j�K�{$�5s~��E������<�%���Pi�?@[o���f�:��HF������`D	'�VuY^ABX%��S%���.l��g����_������E�@&��k��,H����5<���9�-�k	n��
�?����@���� �9Z��hc�e���S���H��NF�S;��H� ������@��%�f��y��s�{a�t{�����D�>��+���@��)�n�@{@*/���'	�"U
P!�e����~�`5��m��
S9�F��N/����m�,�b��X��R�m���8����
	�8��%+m^'����OEu�^F �'��m�����2��������8!�E-�`n	f�<�(h���*[�����"#��T�C����.����U<�Y�t�[4�N�m+��!���;T�K�O��N��;a��s� 0�{�>��.�����e������)��U�|.��7�O��<4L��,i���v����LY�f;\��ak�>t}�H��I{b0�ORN��|=�<'�!%:�-G��#��2��#N��H~����<X��[������P'U����$��#��\9�^�uQ[W�����D).����q���6?���P!����R��m����V��N�Bto�	�>�d�C>�C��l%P�������8�b_Z�'�?��N,��i��8Mm��������x��A�Z�g����j-q���.�;[Ex��}"�4��Z��^�b���;py���-���#��9HI�sl���)�R"''mB����*��g������������CSX��|qd����=�ZMF_����������R�$��
�7�P���n��	a��
d��8�Z]���DV�~z/C�Y�{1[��as�d��A
$���D^4�R���h+P]�Xe-�I�d��]5�$l�M>����*8;T��9u�[�e_\FG?I��d:A���HQB�=`w��*���������)E����������z�M�-�tG�}�W�8;��l��L����1�*��F��������!��.P�N����D1������icn,���]��f���|Q���.3w����sr�F��t>W%0��tC�Lb��
U��C�=�m^��[Rj�gpW�?��=�^p���J�+��N��<VO;�"cN���_d~&t�5����
Y�R��`&<��1�J
�.3N�^��1J�1oW`7���_n����JN7T> ��2�^|�sT(����1�3�z���l% ���#��y�p�E����L��[�
}��(��k�X"D#j����"���%��_��0��B�������s�����r&0�Q"7(��F��u{$Zb��c:/V(t�Q�	�J��!-O���HPv1%E��]%$�n;��i�^[�!�^
��Q5a)o�$QoV)�j;!�k�k5.Q����	�D��W���"5�ij^�����>\�(F��+F��A[�"����������v���(�ng�����P��8S�����h �/7)���I%��7I�r�7��j�p���F7��?@>���b�T�����V�V��-�n�E�;$5�.8^�*3��dq�u��W�)�|�S����b���p/��D����/��X�`����E��5qU������E�e�@f\�2B�U������Zi��Q��g�-��RP�$�$s�@@�l��@�6���#��J&�WhS��y4�s��&N����>9�;-.�h�����
��N+kN$hO�F���-�������� S!)�k��k�U��8�^��������%@�/���?|v_RL���_�n��?d���^&Vy�(�e������G@��5���U��@2!
\+{+D��&\qCP�y���!��1���N^e<���<�o����;�DY����`�S'���(Y`�p���)ln�Tb����E�A��1��O����3��.�msq1�K�G2��*9�gc�	Z=����lQ�)5��%p�+V��k���xE���3���w��?�r������tb�	�m�3����a���l2J��2U���<�$�%���O�����V�ew���K�����U��R��GA�yNp��='u���Z�
�Y|�*:����)���2��Y'�����\����_�!����1C���J��6u��3NJ�*��Xo��<�������&$p"�5�l��x2��FL&�f�� ��D��y�t�*�tj��qI�SD'S9�mL��"v������4��10�����Q�Qc�F���p1���{������$>=�����*KFv���5o��&� � h���'_N�,d�	Y"3��zO���0-d>He�r��B@>r;�Z``��&k�������N�PY8 ��Z7�\/&�ct1P������si8�-��]�Wc��cu+2{�1m-5��Hp�)9�����|eH�����BQ;�2����9sNj�v� qbsib-]�T�Q��C����)&��`��M����AR���H��������"Zr�k����o+uLn[��gHL��������cm9���SE��8G�Y�HH�4��R6KY����z(r��N3�G����w�LN�r�T�l���H��c$@���9��T@��H���POG��/�A@�E{�j�Z�H�w=��X��xf��e2���`��XM�(�^S�Z�^[��a�4dnQv����U7�4�_�U����:��&���A����H�T%�H2
I�V�w��]�d��{�$�oG����@����}H���O3��)YB�P�WOMu�]}��D�����9����Y���D��v��5��e�Qi�������P.0oku|emJVU�
�L2�7A�����"�"@��H��W*�������0R��2�f��%�L�`��F>h�D����RU���������)��`�W?����}������]O$����%�#w-�Ec��g���M�\"����(1���A�
��m��};t3��g���B������<,�������0O��67n7v�'�9W�iY��TH��������sm���y��#�5ip$�Z���A]?+d��b�4MJ_b�����k��:��ud\�Q#k4�0k��r��b����i���-
��@�}��VN|���*�O���z��������W�=w�p!<I����������AB0�D��Y[L�������T2�����]��5�iz�lU�f��e+d������h��Dh_b�<��h�R���+e8�]��X{�B��(��)�E���T�
�=��/P� �>����$�������N��T������4���{�>��6�'���������gpk���o^���]���~(�m�?�0����)~�tw�W���O�^u����R���W��*|�:^�G�����:7&��K��a�8�<�8�3'A���U2��Z�u��[�@<�|?V�V����@v��`��J��*�g��tvo�X&�G�
x*���&�P]w����	y���8 i:�-��pE��r�z�,@�c�aCV��n�c�[D?��.he�'����h��������Qh��U�{Gb���q�X�I����o^=;;:~u~p��������x9�'�6�$��b��I���HXZe����������c����yJW-���
{FwR��2bcT���C� ��9:���S.�A���J��^}��y��
cF���alJ����9�87�a"���.��s�"#B�_�N�@	�F��_K��>��s^ ��1\VN��@��,���U����g��G���
1��'�goN^��>>zuvx�$p��9�A�B1��0����3����B�r8s�����<r�_0-�?<���xy|p�k��F��|-<�k���D��h=�u\�������l��`�-
"�-���lFy"1�dU�^�������i;����+4������B�-�P.7������F�45WD�<���@y�!~
g1�6r-d�������[�pn������F���Fzm�AfmD�����>�
�f�������d�f�}���������jsk>��t}�v�]�o+>��8$axG4��ur-�8l,q}���������� W�����]��1by�\s	0bB���D8A��!;YH���N� ����_�xy������5�Y@a9p:_\������J�������i|��mA�������VX���G����!��aD&i������
�|��hx��������8�T��.b1(
���$������,�P#f�&��B������:���o�=7��r2������
�T�/�7:F��X�Wq��$��ce���oC%C
�G�[�\&��%���A��9v�#��sz��Gr���k��H�c�JB
��@�Q�r�6��'�x:ItP6;>�F�4A4������f���)���q�t��m���%�|_oB#��u����*�����{����F���'�1&����0�)���kG_n�_��Rrx���w{[E
��O�V��l"�E*o��P={��<zD��������}���Z�|�C8�\������38-�������C��Vh��Tq>fL�`�e��:)���
#���gzG��p(�M�����y/A7E�����/^����8�L�t^�&��Z���	U��&g��F6c�@N7M�������@���������n8:�~<kw�?���������5���:�G���|`1����h���!�u<��W��U��jF���h���Ho&!�`LC�eP�#AY��EqW�
0�����s�����l�N<��m�����mL+n�)l���n^�J�t��3����U�Y�|�@/�r�N9\Ww�J�k���\&-.��pc�J���4��<I������k/���\��(�*�4�j;��y��q<��2�#�C��
�����F�7��	�F0�K(��
��2�7"�?~��Ne��h��+-�f��!��� 
�^��<~����r��2�FlY���0��yA�q[^QO�}�oR����;��wl���;��������qN��{-��H�~��~�����7���a.�]r��������5�����v��:}���k�M��:�'������TM:T�+�L7�#���4�,9��D�����:�N�-R��P�P����>�9��M��L����J5]��*^v_O����I�����L��Tr�B�ag:�;�iFr�m}�h�����\����)P�����*��q/��P����K�OCq ����g	&X���@U,#[�����W�����aG����Z�lT����k/=��Y\�Z��Z����k3*��+��iF�s���AH��.��X�����I`>Y�0��2�L��{X2u�~��	`����<?x�@`�3=��1�G��q  �:m��O�����0�
%-��X>Y�G��`'?���}�5li�1U���l.+���y�S�o�����c����Y]�F�vj)+HQ�7�_]8�W�*�������7g��
�]d������E�mzJ�uX�>�u����:H����$��P���:X�/�p�Qp�9V.��&%������q��l���^����J������HV;����}��z�|��s�	�tb�(v��VV�b�N�iq�/'��jf�"��zQ�=43���r"4�������C�w�KR�����M2K���?K�I��B61�:���@:���fsH �����&p=�����74+
�xW(���4��73_�.6:�N����@����|ysm�T��q��?ey�J/o`��3+���r
��SI���Wo^���v��a�b����D�b���}�TF�)��'���_���@�2Ay�p�D�]E�Nh���b�9�Zz�?=>~�/N�=��{D�����O^�&]��@���'���"P��h79;��a�TB�NS.$�*^����8C[���b�G#H3���b��G�x�S��=��l������q%T��{atD�
Og�����H��a6�������������c�[e�yG^��s0�[�
�k��X`�#H�����TS@���z<�gL�.f�e��ik�������	�����h�Y']�po���V�� �4�����o���8��M#Y�e�����L&�(���R���v}��JS�@X�X^%��Rn��r
���R�5����W�!�O5U�OV:��Hr���6�Y\�t� ����	C����A
��%����U"K�%����"�>U?�x���Rf@`R��}�G��Ht�+^z��7����S���&�P�8�
q���[�Q�PE�p�Z���
b��e�wpK��=x5K\@J�Y�O�F�8�ib
���?�����g:o�����K�\�S��(�S���"\�c���-5"B$N�.��%'r��7J�[�/�V _�&��o���Uq8S(����6�y�t����j�K^�*��J
����h����w��jC�I���NKmQ`aO�?a?��dUB;h�Wb�3Y��b:[C��m�D���d=�����(U)�O0��o�w�p$��W��_��N������*bW�����r�=W�();c�_���yft�vX���t����s��l�@�q�lx9�	23�~,[��j4�3�\!�W��k�����=��"D�"���BE>$J���V��|u]�|�=I.bHgC����M�q�b%=33!6X�i���i����I��xn���
<i@�W�/����&6�������Q}��u[R]��H�o��oG������Bq��Y�7"�����>���Jk�a�(2I��{�j��t3W_�h��W��\����R�=Y�{��-c
A������kU��:�a�B2�
��!��@�I9+�kup�v�q&��������c>����N�+V��E�*�.b�����lQ�!T�������yxrr|��O)�YT��?&���������i�
��jI�W1��Ff�Wl8�YC�-_��f�S���!e�E5Xf5/�����������P�SQ�$z=ru!��l��PpH�EK�0~5��������B���Em�z��FI\�)*
�\��C��?X�Y�����t����;��T*�bc�x&5pCr�^B��/�J�����7RK��[d�_�m�a�w��?�6&����>cm��z$���o��BWfsQ��5�J`X��l�j�N@�V	�GK�K-6b��m�}��xW7�]GU�F5<y;V`Z���
mF�-~�3��f��L���Co1u��i<�Zq��T�
E��j[#���G��w0A��}����n�?i8���W(���i����E{�nl�u��y��q���59:-?O�p�,�o�G������D�m�"g<��mp�k���hl���
1��Jg����
��`3Ymd��e��S+X@e_��������5_��Q�7�G�,�e.�����4�b�z���(7���#��=�?YNM�����7|&0e���]:U��61$���3�duL�����_je��c�;����~g]������r����<����5	(���\Q5\�����Q�N5���g�0�,�%x wy:���3�����,\��I�*fn�J�>��Q����Ha�)gHY��5��^T�&���U�J{Ba�1&/�D�sQ��.KF���4������h������)\\QX]���q�s�f�
�?R�r(w@Y�:B��R�ps�0��_[9�p�����W���W|��5q,�_�5A����H:>�-��(!�bk/�!H\=�e=����8��`\�X:�7�M��"�pS����x����>�
��X��6��>�7����t|g���B�'�	��CX��+{��-��d���0���d�K������[�V�@F�A��x��������cw�Y�Ju����NXq��
�����a&�J��V�/T5c���Mt�/��Y���K��R�!Wq��t�E�3�X:Z[I"�lgt��
#c����5`��J��wH����=����;�6X���$�As1iD��f����<���	�JtK#;m������C�x��*`!U�`Y
�.]hc���&�a������R�H`����k��
��
��Ii�o�b��@��j�~�>��+�r�d)@}�FR4"N������uF���Pm��ct-.���4�����[3�I�T�;c=a�sG�~��(lE@�$���T�R<��(�� �I�D���w�%v4�rZ���BP&"�t��We�U ��cBE�a(�5{��R�n�}V_KMc��3��Y��;�K�,����>X�%������+�~`��*��j�r��1�I�%I9�s%C�WZ���'	V�nc{�������B��nKX\~�q�p�V�ii����=�P�6�������?z��[g9&o��U��*y?]lR���{���urY���*���O$��A��U����������S�5�G�x����>T�c����!��l���OS��b{����3D5Bm3'�y�I$���6�Z�P?����XR�*��b������WB>��� ��lr�����6�!�������~�x��~J����?�>=;98;��b��~���D�j<��qtd��maS&+���n`0v���^a��<��)����,PV�����`�~�i��Sh�a���?2���c�gK-�
	�}�<�&cW<���q��v�s;�Fr�^,����"�!� $M��]"�XVjVM���VX5N���&�y!-/&�l��]������������O`�K���p���t5~4��g�I�h�cA�$w��Q���(b����h��s��q��
�Ng������.{��������;������C��������������u��Fu�1u�(H�aG���v�N�U��=�~�=|GT�Gz����n*[�T'y�mha_LIBha��^���S��z5:u�S�F�w�����N���B��p�[}x�D��T{�DD8kt�������������"���("��������:1Et��IQ�^�N����^%�Mr�Q%r594�*������:�����B�;R������J���At��������xS�����nuD�M��UG���nuD�M��������N]���^%|wm,D�8WWc��Wg`jSTZ��������'��[�7i����&E�Q��C�N'E�W��C�V�DU��+Q��JT�=�)	E���������%F��;\a���=V�N����*��i�6�����$�
:�5:��Meb���������+��j���<���<,GMa�<�v�CMa�<�h���R��3�Rj�	����v���{y��w���`4����Ib�;,%.�S��TJ\F�>w�'.R��y$i`��hw����rv�CPa�<v�C�At�]�n�k�vZ��u�����0��2oa'���K�
�2�����2T���:p��N��]e�����2����{{�]�lw�����#��S�;1��T�c�L�:1��dD��::��P{XJck Bi�a
D(�=������0,S�N��2�����(S��N��2y����(}�NJ2��c�Ny��S)���@
;�1-Tf��N�����U���sN'b��w�
sD
1�a��G`��za�.w*e�F�w*e�F�>w�1n����e�v�X!t00d�e���)I����T�)I��������N��|��PT�p%�:Doy�Q(����� ]��P���v��N]�T"�X�z��D��:�����������zb��lVf�<,v��Ra�<,v��R���~����]ZMqX?\S��a��$��p�<������N�qK�,�o�����N�q;�2���(����7o��KU�Pa�<v��Pa�<X�j<��t9�'��#w��~����v�x�s�3����hq8��EP��
��?,����~,�u����������������t����uh|�_���^�"L�r�
������_
����P��|�e��wr�<����:��F��:�^��:f�a���Q/�s����a���u����N�.�+u���z����o��s�R��EY�`�/	{��u:e'��/�~�J�����J��_����>v�>�+�Ru��0b�t�<�n�����'��r���~=�Ws~}�W�CE/�~L�_��j����J�^�_����n�>�+a�����q�V���q�j�S������~=�Ws~}�W
����/�^
��������t{5����W/��Jl�V?��D�z�/���c�T����eP��~�����������e���aE�2t���":|�����c���;	z���������
����������������j���GBH�3�!��_�X�/O,��'ja�������~a�����Y���L�������� �]�6�^�r_�����-����qE��~�4Q(k�{�K(�sy�V5����CZY�<����C�-���^/�c+���tU[Q%���4�<��>���r�_�:�����Tu����_��h��s�2���Nvw-�!�/���J�+�����~y�+���?�	��������'X(��t(	47������3����c��r��D�_���E�8����,
A����2���a���J8+�����~y8+���3M������#+�y���^��f����!F��;��{����~]�WF�N��+�D�_���&%2:���o`i�w�����!��_����!L�!c*K�?�<4�=$TU��RE�Q�'j�u_��
��C�W2�J�����~��h��q�Rj����_�[myy>K�~zX CYJ��J(+�����~y(+���2M�WY������"G���Z�"$Ag0R����0F;;��^x1U�a�0J�e+�B�.:����y��C����nW(����h������=l8�������!j�����,a
�q���N�C9F�F�
���[���
{�
�P4� �U'V�LXu2a���U'��:�n��t�N�[u2]g2��a��	��MXum��kZkS6�*�&��k���&��kBk����
��U	-�JhaUB-B��[u:����V�N��t��t�^]���U	�[���U	�kZ�����nU����UY@�b����4�	�K83O�~'����v���h���h(lBBR�B�/I�
e�a�lb64e��dR�bf���.Bk�tZc/���{]��!�Q�E�41sv��g��^�]L�w��|1�!��x�����o4,^�a����c/X��{��G���?��^���C���U�o�p�S��o`���8m�%BB��@�~��
��Y�?��/k[��N�%D�"k�����]o�m���m�V�m[��������>��%������J�:�m���Ug��]Xcva���5f����V�]E�kPfX�2�����f�f(�����8u3l�:���'�0�|���5i`�I	���0�m1��f����E�\��,�m����-<����g����p�s���\!w�����M(]dQ�+$�@ �.80Y��P��E���-"�m�PS��N��Fz�4
h��N��Fz�4
h�j+i������w�D���M$=r�An�O��
��(�&v�bnb�-�&v�BnR���N��L2) ��D
�$3�2�����&T)7'3���������@��.��0������n�'J��n[L&v�b2���I��{jj��;��i��Z����N�@��J��sc��p���Z�fa��n������G
�*,���m��:v���<D�	��
7,�h������X���K��xM��������5�6�1�a��
��
}���m+�
�V���87o�����5VnXc��5VnXc��5VnXc�
k��a�7����5v��UkP��UkP��U"3�8;��|v�m��t����-]9:h�Q�n[qn�R��8��T���s��M������qc�9V����`����6#�&6���@8yc�KDl�m��m�-���%�XI>�=��:3��:3��:3���j+����Ks�����6C*}�%
i:?��]L*N�b��n[,r�m�ic���yP��;���u�U@5���
��j+���f����'_@@�;y���XQh��+`������hN�b�������Yo�����s)8���\
���;��c�jK.���C4e1���%�x6�P�� 7�9�P��J����P������-$���x{"���N���������;��������0���m���Z$�/����C�rc�KH�n[L"v�b��F%1��T
�d�N��H�T
�d�N��H�����:DRwn���Lv��*2��.o�,�AWM�"��I�_���N�$�"���5��(t�h\���<�w=� ��_��sD���6����:}�����s��H�!��"�8�A������/�Ue��`�{� 
�E ��h����yt���P<�I��8�`
���!*�Ks�������0��x$�s)��+�K���Es��H�Z�)�K	���p.�F���x��Q0�]�pQa1�n6���1r�i@�E'3�8��(��^)�u�D h&�5��r'Qy0/�G(T���=����y����H�S�11��A����6U��������
�dn!*�%H`es+�R�<�&!�'�Z%�2@]�E@��AF?�F��C�(��9lG ��
+�������D�z�;/��	�(��]�<m���a5@�P��8�5�_>L����rG��&�(�!@Re���u��pFK��=�r�O	��c8�p2�����x��2�	'�,�
8&�*�a�z�0�J28���U�2pr9��9������
�6�F9(��yf�5�S�p��)�
�xl��9�	j�k�D�4�a��j`�S���v�H=Y�\�a���7Hg�4_>uA��G�w��-wA�qQ	f�����{S���t����
��?�������J�@�����W.$����"��&�������dsR���6l@T�X���1��z�u0_����v���_%��j����x��?yLZ�R��-��Ag��pa���<i���a��)�<`l�BR"���t�E��Ef�"��6UX�l@����J1C�`X�����C0	���N�'��b�~�$�O���t1���h��� #P�H%����z/6�ri��a<���zI<�hUPJ�����t���~��]+����AB�l6��^�@,f�YY�s�����WJ������yP]&��u�T%�Pl-??��Ll`.���E�`�w�d�	������e�R{2u�Z����k�c��P�(�<�)c��..�j�F�L�L����O�<5g�f���@!$�W$
@i �@:��p�i�2�4T���:���@��B�N�����PKSh�������.�3a�����+7�B��_k
h�5��7�k}/���Fm\�*\�Y{�A(�,����P�b��7wQ{�u1��S�s����=76.BJ,�������w5�J��w7�N�z�L�[�����`jcD��%���1�j�;n��|�R�=���GF��������+��@�WE!�������y��'��D�5#�}���X@���"�.�1�xE��"?����u��>P/11��t%d���W�QV�*�������`4�v��J�V)j��u0q�::H����N��������t�\.V��~�`�_�@����@��������_����� 
��I8�G��z����|����������V�xZ�h�H��n��>Cg�L����bQm���{�F���?�����.��NH?���o�j��y�f�������Nw����t�������3/�����s��x��N1����@g<�Y��*N5r�z�YZ��7���z�Y'�FB�N�9np7����ods8�/������W�
�&7�3����*������"��.����
	���
�~I|�uH`e��+��tV|!�p���`���*e�d=�|)�Io�~�A
��/����L �TJ�(t�I\��hB�*�D�����/w�R������+%��)� ���E���Q�B�����!S�nX>JX�t7|!�p)���C+CI�J��(�JG�*R��1�MX�^��c�IM�D[��01,����~!d���>ZJJdu�Q�B(�o�%�04%���u�N7�Pyw1=d>�����r�a�����W���N���Ur�J��Q�q�����-
���Unzo�|.��$�^L�9�G�C���w���tE<�$���C�������v�Q��>;9<8;���<D��sY;��CL\/f�hz)>��IO��x6��z�LF���tO�?�mM������`��3�_4YlF�U2��J���o���#�4Y�K�xJM�_�q\Ja����^(���n�_J�����������B��t��2���)�J����B���$�{�
~<:�K�5��!�I���{�����,8zuv�������gg�8��������b�)�>�sZ���f>�e����r�/��x��j>H���V[
K��?���%4/�U�0w:�G���|�J&�R�d��/m'��&��-�=��Ng��o\�_%�DK}I�'s�M��%���S�vv���N�Q���3���
<?���������v�w������Qg���_%&�����w�A*(u>��
`�����v�
:��N��g����t��x�J�z@��v��C@�A���8/>�O�Oi�� ��w����E+�V���1��$<�D�^�v_������/�A�;hw;-��b�>�w�"�0�,�<H������@<KSS�����n���~��C���D��+mD����=s�'�?��wap|���$x���6~�<����,�`����Z->���� �Wo^��J *8D�8�H�(dGM������-��h�^$���� �u���I��?��-[������MF3�����y�����q�����W�������7���[`�<g�U;�C7[a�>��?�3���>�s���vN5���3}�{����<�s��E��Y	���#x�����\�d��d�mf�
����i�J%�e|����duN�C?���Gz\�&)�b�������x@���?�m;P�__%)`H|�5�J���x5����t���X����'��J��[Z-�XdN�o/�����%��`����`��~��}���������;�@�o�������-������t����w_������o�~��{�n��V_�f(o0�����_U����8�C�����������/&���
p@G�\|��r�\��U�w���.���P�[�����+?���=�K���C_Jg��	;���@��1��y^����
�Z�����a���*���!�U��>+����o�.-
������u@�j����>���)���p��P�UM������<e3������0i�6�@9��D�P(�d��c��y �_}���h����@:�N�A��A��@
�51�[�b����/:&)����c�k3
���s���m��v'��Q���se�~�(�~]:6��i�O�~4����O��
w���#a������q�1����Nw;�����`��h���v6����L�}�[����:��jO�����mw��+{�t���.?����y7�yu��uP)������>vlp���A�
�(v�>rz������P6����`����Mw������/Qw�/��~�vj�&�U��W���p����v���3���(�����NO>E����	�B�0?W����a���|q;�e#�J��_�U?�"�o����C�{�m{���ZU�9!d��AI;8:���W�g=��P���=��i��<n�F��=�o��t���g�8���?�������C��=����l�h4�/`u�$[�Oo�Q��O���|
v��������`�X�����_�����^�^�����������J�6i��i��+��c>���X��f>�m4��:>y~������?��S|K����5���hlo�_p��8���0]Os���E��!c,��}p��C�C����$P����A<x)Hb������II0�"������B��hA���$�W�����tV�9N���t:���h&F04<=<�y<�%�i�K:��������i4]_�K�k(��+
#�gi��`U����=����7g�<�?����_�<���E�����&�UK/�a���/�'��������k����o|y���`��Sp��D�9�a/��[z���W��F(}��=�� �q�}����������W`���L'�|�.�u@�#�#�3Wz�]�,�~�Q`�T;��R�@�C.���D���i����8�_�&��k����%A��p�X_��a�Y��`v_%���Bcb�74�Y2�\_5����o��I=�<h�zN�����F�����V?�4��������A�� ��W"}���Bv$��h�~�������q}�Vp5����/x���p������`�n�mq<{dK��V�������U����%����=
���i��&sI��3��$�W�D�=w2��RS��Dl�����N�	iA��>$9�z���<{����`6s$��j�#4���f.���G�WHq���av!��w:5t��s>kt�]h��GhN?8~�8�H^�a�������������-+�E��sZ���?����f�Z���v�����x������c%���`�i<K���������h���g��<Q��Z�cE!Ig�=�H���i����d"���p�_-�u~�ZA�U1b�������}���`�:-���K{.�����m�����k5�6cM
���;xq (H�OL&�:!R����EC���Pdm��-�N�b�B������b�N��5��d_�R[���PF����%	��&�+�d4Q��@Wd#;3'T�����}<�l��IL�<A���x�!�k�8�/33��LrN|���E�����������P���fI��?m���
�ue���(�)h
�����
I���Q�����](?��,_�-f0o5�����VYa��4��n���X/=��ktU��������� ���x�I�{r����m��������b�8�,�t����4E�|
�����E{���n���lx���:
����qo#]����r_�;PH7�\���O	$�	]*
F�U�lM�d�)����Ax[�V��9�q���f�w����_P�yF>>���/".�y����T�!B�	�x�����~�������	��s��>��7���aT�)J�:x���������l����V��U�3�\���(���������c�n�����)�/dke�9�}biG�m�����������k���/�9���hN
~&��} 6_�_��
��*��15j�l��e��3���^)#�Z���
#w|�/A�����]�
�d����G<�HM�8cr���,���bU&��yR3�dBd������N4t��~�Dc	����lCH�����C�+�B�� Mfb�@������|�v��C"�l~��M�S�D������������!_��%pO�;u:l����p���K���t��,W���Yr�a���9|O���YM���g�H�\�l���B���a�������Z�������g���Oc1���/��4
�d�X��G4���P�~X�}��3V� ���b��H��y����^��*�G�nwopQ-���#��;�2���2��\w9��//�]�����r���������������=2��r��r��r�3��\��\��\��\b��w��w��w���Gr�K�;��].�u��c��������j.�]6�]6�]6�]6�]6�]6�]6�]6�]6�<����w�:w���X��@��@+��f����|���/�|�������%�koB7��>V��]��9��3��~�$�����/N����������w������Gz��������������:��~?
#23Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#22)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

On 03/04/2018 01:14 AM, Tomas Vondra wrote:

...

The one overflow issue I found in the patch is that the numeric
"distance" function does this:

d = DirectFunctionCall2(numeric_sub, a2, a1); /* a2 - a1 */

PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));

which can overflow, of course. But that is not fatal - the index may get
inefficient due to non-optimal merging of ranges, but it will still
return correct results. But I think this can be easily improved by
passing not only the two values, but also minimum and maximum, and use
that to normalize the values to [0,1].

Attached is an updated patch series, addressing this possible overflow
the way I proposed - by computing (a2 - a1) / (b2 - b1), which is
guaranteed to produce a value between 0 and 1.

The two new arguments are ignored for most "distance" functions, because
those can't overflow or underflow in double precision AFAICS.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-a-20180320.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-a-20180320.patch.gzDownload
0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20180320.patch.gzapplication/gzip; name=0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20180320.patch.gzDownload
0003-BRIN-bloom-indexes-20180320.patch.gzapplication/gzip; name=0003-BRIN-bloom-indexes-20180320.patch.gzDownload
0004-BRIN-multi-range-minmax-indexes-20180320.patch.gzapplication/gzip; name=0004-BRIN-multi-range-minmax-indexes-20180320.patch.gzDownload
#24Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#23)
9 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

attached is updated and slightly improved version of the two BRIN
opclasses (bloom and multi-range minmax). Given the lack of reviews I
think it's likely to get bumped to 2018-09, which I guess is OK - it
surely needs more feedback regarding some decisions. So let me share
some thoughts about those, before I forget all of it, and some test
results showing the pros/cons of those indexes.

1) index parameters

The main improvement of this version is an introduction of a couple of
BRIN index parameters, next to pages_per_range and autosummarize.

a) n_distinct_per_range - used to size Bloom index
b) false_positive_rate - used to size Bloom index
c) values_per_range - number of values in the minmax-multi summary

Until now those parameters were pretty much hard-coded, this allows easy
customization depending on the data set. There are some basic rules to
to clamp the values (e.g. not to allow ndistinct to be less than 128 or
more than MaxHeapTuplesPerPage * page_per_range), but that's about it.
I'm sure we could devise more elaborate heuristics (e.g. when building
index on an existing table, we could inspect table statistics first),
but the patch does not do that.

One disadvantage is that those parameters are per-index. It's possible
to define multi-column BRIN index, possibly with different opclasses:

CREATE INDEX ON t USING brin (a int4_bloom_ops,
b int8_bloom_ops,
c int4_minmax_multi_ops,
d int8_minmax_multi_ops)
WITH (false_positive_rate = 0.01,
n_distinct_per_range = 1024,
values_per_range = 32);

in which case the parameters apply to all columns (with the relevant
opclass type). So for example false_positive_rate applies to both "a"
and "b".

This is somewhat unfortunate, but I don't think it's worth inventing
more complex solution. If you need to specify different parameters, you
can simply build separate indexes, and it's more practical anyway
because all the summaries must fit on the same index page which limits
the per-column space. So people are more likely to define single-column
bloom indexes anyway.

There's a room for improvement when it comes to validating the
parameters. For example, it's possible to specify parameters that would
produce bloom filters larger than 8kB, which may lead with over-sized
index rows later. For minmax-multi indexes this should be relatively
safe (maximum number of values is 256, which is low enough for all
fixed-length types). Of course, varlena columns can break it, but we
can't really validate those anyway.

2) test results

The attached spreadsheet shows results comparing these opclasses to
existing BRIN indexes, and also to BTREE/GIN. Clearly, the dataset were
picked to show advantages of those approaches, e.g. on data sets where
regular minmax fails to deliver any benefits.

Overall I think it looks nice - the indexes are larger than minmax
(expected, the summaries are larger), but still orders of magnitude
smaller than BTREE or even GIN. For bloom the build time is comparable
to minmax, for minmax-multi it's somewhat slower - again, I'm sure
there's room for improvements.

For query performance, it's clearly better than plain minmax (but well,
the datasets were constructed to demonstrate that, so no surprise here).

One interesting thing I haven't realized initially is the relationship
between false positive rate for Bloom indexes, and the fraction of table
scanned by a query on average. Essentially, a bloom index with 1% false
positive rate is expected to scan about 1% of table on average. That
pretty accurately determines the performance of bloom indexes.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-a-20180403.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-a-20180403.patch.gzDownload
0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20180403.patch.gzapplication/gzip; name=0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20180403.patch.gzDownload
0003-BRIN-bloom-indexes-20180403.patch.gzapplication/gzip; name=0003-BRIN-bloom-indexes-20180403.patch.gzDownload
0004-BRIN-multi-range-minmax-indexes-20180403.patch.gzapplication/gzip; name=0004-BRIN-multi-range-minmax-indexes-20180403.patch.gzDownload
���Z0004-BRIN-multi-range-minmax-indexes-20180403.patch�\{s�8��[�8����eY�b'5���y7����3S��
"!��� mkv��_w�C�����u>�C�n4?�h�}x��=���x$�l{�mv���;]>��gb<����Q���a�]���F��}M��~����6��e�q���v��~��~���o	�~���m����5�H�&���0�]�����^o�Y�V/���+~��|:�<�/6���w�'��K��i��`��{��9�-n��V/�M��[�on��r)�u}���e� bV�&�Q$\��f�N�����t,�27�x�@w�Slc>u�����b�2�^�7y
�n����C��f���]/�"�y��#��,���.�-��@Jg
�Z
^����������OG�qrzq|~	d��}[���[w��!�s�+-�/�`�(f��sE���p'���i�2m����TO��M�,��-��m��x5����I���	AW�����'e�w�\K�H�%�����y(�$*6�XI�:@y���D;
2�	h��a�QVP�V$���w[S.A���x"�E�A����V��	38�M�9���h�`��F����71t�+lyK&S�N��E�cX-A1��
#&�q��� T������p��h���h���V�Uev`ud��=�3����X�����5�t��u�H��Oh���:���F�!��z���%��6;�71s`D��1����Md$��&�}�&
�����C�}��W��J��� �A�Fcl������"/E{Q�a�F�P�~a������E*0.�
��p>�^��T����=+���Z�D����4r����5��f�s�e����P�H��)U��h��M�J�4�`
4���c��.P�<Bs�N��v�G+�@?]�L���eD��W*����K?�T��TL��8s$�8��&� ;>�K��5����`�\�.��U��+����nw�z�t�HU��x]����f���;`H;,�t��*Y&6v��^�����io0���1���
�����Y�7�mY?`<�����^3�v�D�E*��	8���]y��d���^;���R����e�\V���w�9E��U�+
��w��P��C�����o�Tb8QB�lr�c�H���2���U�����&���{���on�����o���k��bk�52��R�����L� �6��HF
�d��*<H���1���3D����qpA���>-v�`�F�r$�=p3"n�{���h%<9w��%�r���B��q��n�g����PS�����6����q�<9`�}��;|���OHn
E���q��v�P+������.�f{���t�q������8���S@<�/n�l��K@��!�| j��n�v�C���c
�'���E��8�C�$wce�Oi����K$��X���/-��
�3�����G�w�!f�
�G�&�`��%\�+����HJ���P@5�Y��v��Q�9:��F�b�������Y���`�,7#����O/M�3T�Q���9�j=|'�^6����!7��:jq���]	���[���J9�H+��U�r�I}����t	��Q����cY�������f�q���p	ua��q�X1{zZ'�,�\�x�:2��.9����j�1,���H�9E�4��sT"@T�E���]w��z)*J�52�Um��M/��;+<�@j��j.,)��Y� �\8!t�C~��R�e%_\b�^���X���2lU&fq�Mr�6��tIU`��)�_Q��K�����K�DN�W.����F:�d2H"�i����L��0Z�
�Ur���H�$�t	��T�t#�T
�Sr� ��c�*r�����jSxSa��\4����a'��?�En#4������DS�_)6)d&���W������x� �<P�%����D	�����-	�m����+��m�M��as���F��-�E0�`1*G���&�����z�+<���\@����-P�R��t��Kv�U�&�Z��@AoY�.)� M����
�rQ���t������z�2T�zP���E=���$H$3�j�9d*r����R^���w�?�M�v]����]�t��u�3!�tP�����-��+��i�eX2-�@U�%�@�5�|n�&��LX-��b�����t��7�"�lu������g";����
F=�N���l��D>�
��j��{�B����P���IM�Jfv�(����G����*����(�`��|��W����������L�����o�tA���}�lj��4���QSs�f�o���7���Gb��f�V���O���!W�)���=�I���m?�d`C�
��R��S���p�)��m�X*
X�����j�\_>�f`��0�$Y:�.��]nj�\!e�����l�z;� jA���V|g�`���D�l�\�������������t��2��Y�C~FS�da��3C��	�.��i���DnBd��u�j����	 ��(��YX��:�����v������2��C���
��j�E��Y��[*����T��?H����^�j��������}a�OO����}e'�G��������������~��_���������rrqy���Y���7fs�my�0U�� H����	����X~����TYs�w����BRE|�������y�S���|i�[Q�/������1�4���46���0V����1:&����f:��?��0�����A�\�$����n��C��%2�(��|��7����3T���'f|����mfuN;��j�����_n!� fb�>fa~�����G�����=���mkwd��+/c��DM�;�4�����&��8'�'d�{������j,��Z��:5cX��n0�.��g��z,�u��>&�F�������C�+N�*�7�B������ ��luJ_��v����V��Y�� I	��v���I�3������~����mF���������v{f����s?�h��E?�`��'
��E��-�z_���`]����0������R��B g���������������''��`f0��`VSK
�~�9Et6;��Aj�V��;;���[�.��O0����7J>c#S�HS��l!���b�����'�<�/Q�����?��	�����z�?J�2�?���=��
�`��8�&��!c��6�7��tG�����7Xo���|�����/��I�U��x{h��}�][\u��u�1�V��9�mv��6�%��6:����*13^�TN�����I��#Y���{���T��S�^{d�A����"f5��z������	OHo���?����Cq������0����N��iiV�>���@F����n�"�i�p��<.���h������8y���1��}�`F�6����	O+h�,�Q��D&����sS1�^Z\9����w,�o�]:����k���a����_-�3r�/��n����$f��1FZPHN�\�R��J��Z���^@=�Z�t�9�%g�/�6��jX|m��6s�����j����N<�H�����5�Gf�[<�@�
�7��i�N�W�����?�G������yme��U|=�@<P��(���c�"���y�S�.�I�
���T*Vh��%O�>��M�
���n<	";�"S����^��0��n^mM���]+�F�Ks��Z�}��L%@���,g��B��x�%�]���������*�����6�q�X��
��uJR��E��7����H
��YM
�V�V���0!]��RM��T��B����������Ze����e�U~��0vM���'.�/��3,�5�������$�@�}e5��4r��5���*{XWSCDX7�`�pa�`��d�+'��r�)���6�3�K�N"�%��)����Z�����h��Y.a�����V�,����rAA�H|��������t
6�e�Q�����������2�-49��T�&�:�w������H��&�t�e=7
���k]E���-�8������c��%5��Lu	�9h~s��Yb\o�sS����|{J�O�+	JQqB�l
^�����SVr����v%�.zxT:�2��U��v!]��U��M@���� vb��&+Ff=�*8:>�<yrxpyrFaK�=����T�n2��P�3����5���fq
����-u��;��F6>������w�/����-b��r��������:��p7�����&�JoR��������1Yz��K�a����'<�Rz��+����G�=�rH�-p��o���4vtP��@� ���8+��2����
_��:w��B�:\���&8�}9��l���\ �\� ]�:T*ON?�2��>���~�xQ��*�W�U�["�V>�"M99:��<8=<�B�/���%��j�B��<m�c��4��`�k_��)�/g!�
��"�����^�S����{[F�P����)������:�RV+�kT��$��7����"n�;t�n]�/%�����Q�=
�Ib������A*�=�#�@��|���c�k��%$xfsCmC#�?�{��:��B8�0�����f9w��t�������?�Q��+�k�>�a������_��>����h��s��`�e,�BM1����1!��"�t]X��Ft�EP�g��y����D��-4���BG�f`A>����>�S����JU�	E�H�L�X�����Q[%2m!:R����
��m�����4`���*lk�3]
���������������:D��K���Z�0���������0,�)���L�B���h��w�
��G:�2�:���,�����X�������g%]4�`z�o.����_
�B�Y�0�j*�S� ��G��f�����}\��(��Pt�����)(�8��DKO�,�:��q��,1���k�����,�LX�B��.�J5�I�!�u���#	�YR�J/���^��q)�t����(��:~�H)Y�������X
�M���$��^/2�,��:]��7��I�.��V�������?�,�rN���g�������H�T��{H�%Z�<?���������
qq��N��:��������5F�"�"V�,nPR�2�Rc���y�P'hO��n~y�����w�'���N>|w|�����^�e��-w���5�Wr�������T�C�Q
'��m,�[��;\��<�3D���0Y���������S��9���zRM�i��~�"�L�������C�o��o�)�b��Pp��H�������;������d,��W	�s*��<����0�:�A�a���W+`���6O�r���"!�������7��1Ve�oD�|S�r��m�Q���14VP���rz��G������z����*���E�F��gu$��^M�V�%0�XJ@�)�+���:�(���f�"����Z�*����\ ��pKHmT�a\������Z����!�1����b�f�N���*��t	m.1��:~K�_I�>�Lyl���`|��*���|�9�[KS 6C�6E9� 
�m�T������(�N���VkJ�a��N���m�,!0U��r�e��r q���Y�����	��l��\+G7B&d����h�Iv��A���������}��iB��7,��uGu�R���j��������M>K;�gn+&E/���^�3o�����J\0�c%��P��V��7�r�4�^P����c�MCb���"��<ju4K&:��j�v���m�rh�P*�"���z6[��������Wa����{��6�$Q�3�+�5m0A
UXIY�P��i��%)�=~><X�,��H�����/c��V�T���L�
�Q�����'�s��)�X�����������<!�
N�9����J�x;���7.a~�cn�^:�T�]��7������
[�������+T$X7������z��\����� c@������:w(�
v��M��nv�0�l
I=Y�hg�5��T�R�-�t=��b���a8��`_	
����T��d�$�M�c��Q���a��"~�O����������$H��.qX��oxpO�;a��l��I�8����4��k�����]��<<�U~pzv�?J\:���*K�����+*����bic�pLCpJ��w�IA��[��� O�-�����t�*��wX���QCP����^�����XM�����)����Uu�N=�2��
� ����T�Fv���jk[���%s�05�n����T|$�y�@7g
L��m+$���z>����6Dg�V�a��&������SQ�����4O
���f��J��ft	�����b��<�@���b��D}����d�.����"'���S�p��Lg�+�@X��-��C�R�q��gZ�Cq,Z�����^�{h��;sp.N�t�l����C;O����MU
�|�o��C�D�8'E�>A�f���-*��}U0��
}WC�`�<$`h��y���bn��6R/r���~
7Ns>y�����LC�XL����jy9�lH��k�(��E
��S	,�*
t_�������f�+�*�23��d������2)�[j���h����
��oh� 1�u�k�����(���v��y�1��&Mt����wn)oqFj���/�UNP3Zw^/4��t2FS�#i��)xl��SM�R�?v�����r4�LjI��'H!"�?	� Y�Q<3��$j������9	��R��I��'09[�5J8��@:��w��z@l(>����w�����s�`#��a�<_zxha�zS���WH�3F�6H
��?��~���B���}���J#����3�C�>�������`���bX?��AA��|-jx�������7z;�	�/����e���������!
}\��1p�
��/�V4�L�Vc/���o�
]�f�����},2Z#���l���:8���j��f�)4Fc_��F
����g)�S8������� �d�����	b��<^���,��g�N���	"8,f�4���M�P��1?F�U�[	����"j����0i(a������>�<yB ��?����c�'�����g�#�cY�0�H�V�d;x@&~'�`4��}�ys���Q>�T2�6��d���7{�eDw����}�C��#7�3WE�X���0��*��p��vb����A$�XT�K��y�����F#��!O\C�l:��(f���
������.�W,�:4�M����jp���)����_�������:&����4K"���A����F���dA�~UA��9��.~&�RW�VrP�����Jr^<U;��������N�����X��o��P#���n����c����;���S^I��*�x��{3!�a|�v�KRw�P����M�+��t�
���W�q6�r�������}��<�w�������*��v���i.���QU��s��`#�y���L�5� /v��A[��I?_�����2S?��F|�_���Gu��
��y�s�:��C~����l	����nX/�����b���� ;��{�w���4�T\��UB����\;�^����1S���p�����e�_(&��2���>[|�b���k
a�d�9GM�kpM�_32C�t{8�
jQ����������/��
���=�p���s�l� E�o���7�N
8������d��v&�f���`,�����e!�e�����1����\U�Eb��k�-�����e�9��w#Ab���W����������!n��to1m�2��*u;��F�&.wm�QJ���$�5������	Jn�GF\��)Xj������������hv�����/�_|||/��8�D>��1&��1���C.Q��<RB�_X�p�.ML%�0S�,g�2�ZI����

�0�+H]��)=$�7<���o�yz�R��
�'���~x��)G&��z �W�����
����9�x�����C�������8�hE�+�d��v������qd��a�9�k���K[���y����ou�"C��9j�F*�W&�M-?�]�r���wT*m�o���e��+�`�	�Qu6�M���R��B�
<KLY_��
DU\��8aZ����)G>��C�l�k�}��6�?�XWK�[=�tk���hv���\��YS�����������V��;�br|���`�EK
���Ii�	�7W�<��/��R���+��Z*�C�+99�j�3��r��x[s_^�;��w��7�������9L�.�pi6���y�{k�N�~�8��,�PJw�����)�u�J�%���Y�
�,����I��y��t��Z`��p�Mb��Fj�Tbt��@�����fm�p"���A;�	��a�b����9y�R�W)G��M#4j������%��m���<3$�k)�HI���%I�����&)��%&&�)���e�"���i�c�AT?��D��t\L�v��rV�&��bL,X���-�mX�rr� �F2����T���f��_�����j#�(�5��w"���C�>��P�����8b����Ae���Ro��m�qW�?0��4F��1a��`�kRG]���:���X?/�JnyJ2Hl�kT5��]����$V�_�G���*�it��6�V&+m�;�5���U{����;YE�b~��V'�"���������������T��
@���C��������Z����.�%*P��l,9N�^�u���
D�~��m���q�N`�\�	���W�����@p���>�a�F����s�Th�GS�(��U	����[	a���(us
Y3M�J�X�[�9^e�D�#������?�
�=N3�NV5Mjx8]�i�`>�!f���������t��zp�TS�o��-oT�,�P.E�q>�+��������z}���������5���eW��L��(Nf4��Y!���#&��J�B<*[^�
^��P�G:����-V���Dg�j�*��g��&e���R$'���O���hM+O�0��QY�|�N����*�������]f\!bk���-�`wwW�sG���L��������"���u����2�m�E���,�����g��<m�+8����P��2
�2���sB�a����E��'K<�������c�]���:us
O��hQ��`Z���jU��I�<����0nyl�	�_�K���9;�88;;<=�x���g?:�.qzrz���W,�������h���W$A����K`�O.�-��T�@[K?WN���J��VK��������C��X]^C<o:�$w]������-g�����8��,�AO�	�����q�
%(F{���Y~�Y�(�a�@��}���E�I����t����#T?���!Rv0_���i��B��&^M :�FF����I��6�n��yM5�n����5��c.Z49����	0������v\�2���L��5����Au/�d��Rgz�u�w���"���\�<O!Gy�oK��YR��6*���7y^
o{������.L��z�"������XOD����Eda|fl�6Zu:�rD���"��6�E�	����tEC��}�*���V���+��b}b�.(�V�5�U_�z�d�����q�F=>����{@/�\�����Bs��
n��C�.��l��[<\C����5�v�B"�Y�LBQ��!T�J`�#h�����o
;����Iq*�sJ�v�c��W����!�>(��H���3<����q�!
�G%JV�$�a5���r2[-�e�K%k��n.?j�?0b�-Mb �;���b��q��"�D��V�P�������9��KQI���^�]��~��v����������SKS	KN-ip�l����������n5x�6��[O�:]�b%-�WT�InK;�L�g*ACo��s�&>QK�<��i9�hL�8r�$Af>�v�m
�&N�~`�s��C�����U=��XG�[c�hF����)>�|���+�?pL$���|;�A�|�G%�n-1�D.1�V+5�m�+��s�4i�w9���GJ��e���0B�l��Ja���V���������c.�=�W`�U2U(F�=Nds�
��e�!%�:?y~��U����H�5�,��,O9�G�`-��z�)�O`��x.�xl�6��\$��3�����T��KGP]����NK�s�~������?;���#����JS�&� ��U���H�kf�l���?��Tb������P�?@[o���v�:��HF������`D��4Vu����5{�k��"�p�<�����
�H,�D�d2���\����Hr��YN����m�^[Hp[��-P@��IuD9_Q���hm��� ���N)!_��0����gn@���A��wq<����%����Y��q������\�
Q������0��@��1�M�E�TY��O�E*U�C�$*o�=�b<��0#�v�
@__���em�,�j��D��2�u��0��3�nC�<$�q
���?�oKx���t/���~�������6���V�����8%�E5�`�	f�<�(�T��s+@��j /�|����&Zv6����r�������C#���q�Jr�j�K%�����;�8D@��	;���������h�rL�R2������~��m�D�^��s~���`Nyh���I\��?��m���z�8v��[&#�Rs���U #�I��g<���1�j�yN�CJt�[�ZL&����:]G����1y���7����_�C���U�'��evuF���v$��
����9�m�	8�2,.���u�E�]��3��C_�s�F��L1�����0���RuRo5	\Hp43!�!�a6�����a�x=GI���XOb�\�j9M�l*��z�4Q�[���#��x�E�F�g�����F�8��}*�w>vJ3���Dqi�I�L#{]I������m��<����L -AO9� ��f["�DNI��h
�c�T���&�4_?TGLT���J?����������G_���KY�sr=_���L��,�G
J-W|�	E�	i�V��6���O��@@���|��'\����=�"�T�b2��z����.C4����|M�B��_]��@Ot}k=���%�z���n���m6����W��Y�CD����f]�&UgL�;^G�:�.1x�E	N{����Gq`�fS�T�V/0�^+��f�&��i����]��TV�0;��\�"<������j��F��������%���.��N[��
c�j#��q�Ha@����*� L?�Y3<
6[��|e*�L����~�/� �O������eg�cI��E���=�N���@��_����ZI��k����'��tS����t��{)FO7�"aMtw}���L��+��;a\xS_�a�;��1�J����N�^�"7�����/��Y���FG��|~�(|@�9�^}��Sh�K����3�yd����W������<wpfKm%O����|-f(��%m"�
�?�!C8�N	�>	�"'�;��U�0�*��M@c7�Pr�x!. y���g��������h���i���Y�Nf�*X��5��<V�Sc!��JP��QM�(�z��[:-��#�����uc�@B����%Do{A�+�t�.��h��
�D���WN�9���xi_{�W��>\��F�3W��-�v�E����4R������Z?K�D��]Kb�I�gL����}+w�<D��C���e���ed��yL|g���j0RMDg���w��+�pC8t����;W0���������v'�w)��)�����B��Q�F�k)�7|;�(lF��2_���S��i+��`��9��K�i��H\�W�1�� j���P[FJ���w�!oL��J9vE���-K����vx�Q���N��Qa<
&6$tC��l��@'?������*
hus�}/�$�*�B2���9�;E�fj5��)��V2���R�hq1f���#@�g���/���"[%L.W���9���5��q!�����m�A���I�%U�4�0�,�|�e�cLY��,T����f����r{��XI�o���������M���M��"��X����|�V'��cW���2���5g3w���|������`�G��9�"�0��n����D��k�]q&�V.N���O�I?�g��]����r"�v8F��#�ik�uZ?M5Q��	�b+5jcu=J8'��],:Iw�U�9��
����vx�(��A�;�����8��h.���������|�d�W����b[�g�?\���2��gZ�N\��'�2�YI)&.^5q�:+3�)%�� �=#nF��*��[�����G5������L���]ntC��/��9Q��@��!��g�BebK57��>�x�
���fm��g�x
�NA�]H�AgV��R����(u0yZ�x���1�[f���P�T�����O�)�uEu$�6;kbO���V�4�+�%l����3�G�W�Rr�&#z����#����1�$��������`���d�^{+�76a`$@�0@~9���T6^%4T����H�V�$�IulP% S���a���+��9�rx��iE�_�x�����r=�/�Y��J��\7�O����Z��g���A����Wd�cB��6\ � ��B�������oc�T��D-�
������/��A�����F������]����U�KL�������d
��8�c�"�u�rJ�K-������n,�&^���2���/�����%AxK����c�[�������13q��'aH�4��T�s.l�qR949���3%N���l6�-R!T*a6�f��0������|�A��B7���G�)�x������]9��	����N�F��{���|�M
��3,��G��3�����-�59�������[�ms�����]}�����O��<#��dz?[I���v`i'��!Q��UZj���cCw^�]�n�b(&�(9,�=��v�X��qx.�����0�7������<zlk	��K/CA���=� D'��S
�8�jo����<�*�B�aH�Y����������Aq�e���h�`��
��T���	Zz���s}��R�v>�@,������&�f�S�Z�� ������E�<U9�-|J���	�C��C>]�WD�>�&p��&�>l�/���kuZKE?3g��U�h�s1�UtP�����%�\2�v�h�p����B�����pf�?�Z�q�Y�I9���M�^A2���@"-G�
�{2����P�_S��I8o>�}d�(-����0�#j��2�M0��J	M��jN>����|Y7�l�G���G�@�i�P����%Z������>���$i���*�OO��yu��������a�5|\X�%���b?S���%$@�P�G-��b�c��7����&��E�t���z�Y�,�w�Vu��R��K��
~��H-��'�@��0�!L���&�o���te��`5
n������	�o��*L>9���JY��~c#%)�L�^�v�Xg{�C�r�\G����CZ�\O�C�����'������/��<����x5I-T�v��}+��|VU��;�;a1�O��v-5�!}K��S_z�i��}�x��+�����(����F�d�����y)K��.�	���<������
���j+�YMdg%|��_(�_���������'�`����j���������7R}B)iF��c1�LK�J�/���W�������w���3�0#'�*��)c�Z��M@{����<W����G��N#W�F���6*f�j��a3��9����o����_�~w�F5�����������"b�������o���c}��(��Onc�T�)������;�tP2/�%��*�_N/ �Y��=b'��-5� �����W?�<���z�NkF���l�O�b� ��FAOv����b��2Bb�B��jJ��;�`�������{���p<�7�o=��jW������=���������9=�x}rt|~xZ%p�;�A�Jm������$�����J[pr>3�L�^�,r�_HU-�;<W�]�xyrp�����[Di-R��:�K��D�1Z�h��y�E;2�7P��Q,jb��.����"���[5����U��P�9��[��
��n�zY+�YiK��%�&)k��s�.��+�^��z���P�����j��^n�4�S����5���f(gm�Q��4#�6� �6�����
@9k�i��M�e�$���}�������\1u��Bnz�^;n������B�Q?$ax��h�G����D��q}������������v{E����$`������!cFM�<�G��5d3+I�Y������[���C����x��2��x�O./�dn���D��~�{2i}�4>J�mE��t�f3�����E��%�j#��cD6ir�(���
����h�w�������$�T��.j1(s
���$������$�P#fUF���������u?��ys����K��Z����j��pm"V%��~�D�����������������r���x��q�w�i�s��DvK��xJ��w���^����`��>�4&��3�+���
�Q<�b�-n���5
E!MA���{g��f�����:E&��.v��
�p�
��������J\�m%[E���SJ��V���'�}L�'�o�`��`]'o��
��xyx�����v�����) x�O�����#�u*
���P?{l�<zD��� �|��o��C������dD% �j�[-��U����`��b����t���Z.��M���D`���$&w���*������W��1o�h��H���������Z�~K,���`����Tq-d��%���
49�M5r��r�����Q��
��������
G��O����������~�qr
�������78XL�x�+���a�Db��e���vd�����m��������0$�x��)���u���+����cFg��cqIw<�'����ne\f�V�%7�6��N7o�J�t��&����q�Y�t8C�|�K)��j��M�lsNi�����<�������rvu_���]5?^�� �*�4�j'��~y����h�H��z�������
]�P"(�k����7h'r�+nD�����Q9Y5�
������Yf}52/�QtW�������O5��[2Z&�H,+�w^Z��?/�6��+����7v�����}G���}����}����}�}oI�Q|/�����{���|3)������O��Lr���[w
�n��Wn��M����?�>O�URO�s������t�XW��f�Gb��h4�K��G�!)t���[X��P�P���nV�	wM��a������?���o�U/��_�Y�����a`��5l5fNbT4���r�0��\7w����������]PU�Z���<+��t�����W%^����@�65�>ObLpx'p'��>G��]���W�����aG��Jqy�]6�� ��o/=�����,�J�S�A��3,�����jV{���BH��/�X��~-����~2pdTom��������}'�����yyq����fZ�-�{B����@A>>Sm;����G���0�
%-�������pp3"����>��6�}�G��>6�������OL������`���������6#[;u���������*cPw<=;y����A�EN�Av�lI�^�����[�������;__~N�I�>�[��g^��I#���^�/��Y��]��tYp�aB
��+)ft�A���z�Q��)�r��$(��h��J���"�k�\�'�L_�5������u)
z	'Ac�������)����L�`���$N��l�Z�����>��(������G'���F������S�`���`nIg�7y���R�-+�U�g���7y�9A���/<����.A�D���{*�s=���4�oh�r*�����p[�OW��r���5jb�����m�������0��J��Y7��g�#7�Y����������3IE�o:9�s����T����S3�S��p����e�F�rF�U�
�w��c�����Sn��9Qz4��D�[��N���o����Jv��������~L�d�t�u��5s�#6����W?aDP ,��g��rjR�!v�K)L��)��;HD�)n��L��������VJ7�G��qS�=��\����L4�q%tJ��{at�\�M��[I����c����)�$���j��qK??<?98;�mh����|�I��ts5R�W5�q��,"�iQP���V������Kj�]�1������va^��_���{h���L���MR�Y��7�^���fp&�||U�V8��Av[��{P"1h�sL��$�j������[3U<L.C������EE�������YD�qk_�a~'��C��������h�>��In�������@f"rX���
�P��]|�����\/�
�b������K�+&�bc�7N�XL
9���|��o���:��r�����p�E��Yz�HB9�r��#m�Jl�]�fg��,��b�d`UI���������%!���5���s��Otj�%���g���K��Bd���msP�.��	g�����T�T�I��\��?���+��Mgd����7�)�������[Vp�*��� �����vt=����+Q��^N�d�?���>~����N�6��
*�����q���i�<@�%��J�V
|���/��T%���E��Wr�`�����tM���Pw�Y���#y|�������Jv����&v������;����ZCuK��i���$���C��`���<����)��r�
r��$��t��)d]M��1��U�h�(������@E�<H���N�:!���15*���(�J���J+Ec��t�Q|��;��i�`�'~T+�23�dcE�
�)F�������|���{S���dx�n��Jx`��(kE����g�U\_�
 me6
+�S�~�������!��+�4���J8�s�KaiM1�E&F
=pg��ZO7q��/�JP��B�wt��a����0�W�i�������t���T	����X!�Yy�L��2^��+m�Yg��N��O�]0������_����W�;9��2���6V�JI���w���U������<S���/E��=�gO������k��
L�o�T��\��g#�|i���N��5K���v�2j^`���A�J��+�8��L&z3r}�"�l��P#�c�<�_��[`�7X�^����Z���wm��e�������8�?������]�L'l�X�����L@��e:`'�OX�$��KMh1��R�J�n�!�P|�#��YX�&����s�����H7(��X����\��2m��o�a���K,�{/���
(�+��hc���Z�Tq���S�/����~�A������
+�-t�m�~����&������������zR�.vnA�[����5vF�J�%3���`�����|h�^1p����/-P��1��w3�����y��
�4����+��:n8�����X��'G������F�]�*g2���p`J���`l��-���jy��R�V7�O�^�Xs��y��'�N,�H?����S,e����A�nP6Y��[JK�&��[q�����	���e=��^��O�C��l��F�
�)LY+�g�J=�!����|&�lc��?$i����
������gn��'������2l��r�4�����	(����U=\���������%�Z�3���U�����d���u6�#�l�ub?�������������������=]�Y.v��k���RWT,�'4��k�l`����fY�,������~����G�y��r~d}hZI��kA7*x��U����jB�!�hi��^�����NQ�dBe|�+�������8�y���%L��������_g�!Aq5����$)�Y���AOvng����V���U���f.\��V�j��%�raD�m�r���]�E}�^�N.��]^�|�+���>���6������5qK��1]3��7Q�+�����JT�
��Q��R��W����Q�������'��st0���sA.�t5s��@������@�0,������#C��C��C�|�3��%m'������|s���p�_.��p��)��S$v�mlUkOx�0q-�H����o�%_�\[���:���������YD�N�5�v,�7h�����*�T���zT*���.ji���3�==8;��K������o���:F���uj��jy��f�	����������&p9�I��?[uw��*5��*�B�,j9`�?F�+�'�j�t���:�A�J����)�1����z���c�g;��s�w�����.W;~���v��^��W��T��x�UQ��A�#?�Bv�#�T�rj��RQ&"���/���A*���>����4k�6mY�����\�Mc,�;��[��?��p�EL�e"!����)��c?p.�Ulk��J���.g�/%ku�)��F~���b,�]��(j���f�E��))6�&��/�z�6��2��d2���_l�D����%���=UN�s�c�FCFI�/����z)�H�����L����<���#��N#T[E��#d}2��Qq5B^*��G�����3&x�#]����M���i��l��z�Y3��Gh,o��R�c���TF�m��������g�f��)Wh~6_�,$)�X��Ai���&w�Y�.�g�
������w����yX�z����������w�P�v�����FZT�Q���#����s�
YYm���L�9�K��r���8P�q��Y����������>��/�b6��G�����Y��@�o�L�*�U�pu\���{������gs�����/�`8Q��n6���I"d�=�K�����u�\U��+iy6������
6���~���%�	�\��sw�?Z.���f��?���|�������B���r��A��(�"�{�����G��^/�N��`gg��;loo���w�����`�	��z��25�ra``Vp2_-R����������,=$��\��������Nm���rU��a�������j�\#����<y}~���������?'k����l�e-����:$������+�����q���'�.���*c�1U�R����D��N
�M5�w���y�2${�����4����bY.p�����L��������pow7��f�2I5��
������4��.��Q|��C@w� )�)�)�_�fdRP{�K���sL=�$+��[G������������X��}|�v)����9?��}��r�!�,s���������"����g����p��7�f&n��>Z*���A��x��;c������������V����7BZ���n����������'gG�G�^��4v�������|sx��_(�j��$u[J��B�&�~��f�Z�������,? ��o����N��4�aX��JP�V-��p�_}I���j������>�(�n��lj�]�
]�Rg'��	\z�����7��f����I
�D���:MQu��q)���"���"_�����gC�1gl�//���n����u�qFL.�����U�2h$f��)�$���=��d1�*����{��)~W��
8��p>%(^�pj$i�F���f�D��vNfz��t�������GO_^�����������Jxb
����CEJ�~��hb���"��7�vw/�~��jf/rBr}�m`i��-X[����>?8?�����J��nU��A��`Y��j�4��F�@�����a{�-|G�A�6��v��a����;�����������#�Z�7��N�tjm��)�tjI������Y�S��{����Sy�������m��%o�dNm����4Q~xMMQy�5
E����I(����4E�������"��&���"���Ce��N
mZP���S[:��[hsE�������SjYC�;�N�Q��������l�N������,��)���(����,��)��M:	E4K�S� ��W
�MQ)��4X��m20�)J-k�EAT�A6��B����khm�&M�Qg�Ck�N���,q��F=R%���Un�T�*�G�De-��`�R�`�nN�R������0���*"a������*��:�#�	�������:���Z��"�����N���T�Ty54`Bp�I�&�Sjr;e�&�Sj	2f�����)
�WhK�V]".��f��.c0�5
����l�
�����7���-������U�E!2���XT���NY������NY���.H��=�u����Bv�����
������/��v�
�K�D�Q�(���G�4�DP!��k�W�r;������N���E���I�v�}��Z����h#<�S�s;	��T��`�Hs:	��dD��>:�
�5�pDh�=�Zc7@����r�m��"���$�(�h�N��"���$�(���N��"����%�N�9����@r;e1��NY$�S1�Ai��n4�w��j:Y{��D����4�ha�H�&�����0F>��vjJ�B�mujI�B�mujK�4��[�7_��&V(L`��mw�BRn�,$�v�BRn�,$����$�4�d(��-�vDoY�Q���n��HW�!T6���'�SS:H7N��t*�{�Nm��f�O��^:���P ���NYX������NYX��������).
=����g��Xf*L���:@��q����$�P��;��m�(��$��/�Om����X�d�6
�T	�v��Pn�,�v��P�%C1>	}�������A7��B&bu������gD�{!�b��E���)�ID�+4y�D�k�6�'2���+�'b_�~��%���/���S�u�=�"��!�J���_
��e���_

���]�avSq� �3!j��7�Ou��\����G��X�j�$#g���\�8����?��B�����~�}�_K����~m�W����(�t�%a���]�Qt���"�W�������P�q���_����kK�B-��k�"�K����������~M��>�_K�m8�����=M/�~L�_$�6|_S��~���+�~m�W�l����u�����I�r��������~-�������>���_��
�b��fk�X����p���Z���/�V����g����Y?�K{��	^J�]�O��)�u?�K�s����^I����n�$�y|�W���<�[�y��	^z������
CA�~�w���~�w�B�~�w���~�w[���GJH�d�9B`Q�,!��_�X�/K4���rR:����]���<d�|���|�ld!t	Z�Cdi|�}Q?Y��P�O��M����$D��	d/��q�r�z.��*���~YH+�����~YHs�e[	�k����6�j]�UT�4;-"�0�"G��:��Xn�UG�_S���n���+T�~m�WD��I�b��
�B*���_Q�,����_Q�,���@
��(q�u�4���@3��m���8�)�-&�_S�&��Z��(B����~Ei\�0���s`��P����YQ�,����YQ�,�����$�v:��������k�����aT�^��H&�K�E�kJ�"J����_%z���/��aS��#�;��PFz�K!��_���e!��_�
��4�I�T

	e�B�RSc��3�=��J+h��JFQ!5�������~-�WH�n���+r���.&�i:=v���,��N)���BYQ�,���B��G��$Ev�q�K���GV.jI�g0R�a�v����fk/��J�02��,Fn�)���r��z������1${(m6�2�~�[
���]���
;�03�p>DPE������%L�:!���3(�ja���a6���a��a��a���d���	�N&,;���d�e'�,;�f��4�N��M&�iXvm��k�]�����������	������	�����5Ec,ChaYB�ZX��B���<Bk��N��t�e��,;��3��W�!�fYBk�%�fYBk:�V��2,�Y�4���fY�tX�:���~��%��'l�v��x�a�
{�X�[4���d46!!)D��.I�
se�a�lb7�e��d��bf�=�.Bg�9t:c����{]��!����T��=����sf�����(;_��9�_�a��[
���j���9�_��s�?r�����3���������=�jb4��;9\�)[��W0�?}���!!�a l��Wo���Y�?L�_�6w���J��E�0��q���~��%������6o���3��!|8�N9�m���u~�����.�`v��7�]���B{v�n�����p�7��p�m����3������}85l�9���'�0�|���5�`�It���0�m>s�&����E�\��,�m�����=����g����������\!�9����K(Md:Q�+$l@ o�,�G(~�<B�����6K������C#-94����C#-94��ei��HVz�?�"I��%�
9�V'3�'�D�
fkp�m>7q��s�m.7�I��'�C&����Ib"9d��H�8m�LZF*����q����Qb�a�c��Kd��<t�V��`�D��m���m�O&n�\2�0���Z����O-Gs�����<��S������_~gn�;�n�XZ��t"�6[Y�{T������6����-P��CS�!!�������6_�u��K�n�|���-����m0��s�m0��s�����K�
����-97l[rn4�=[�6X��+��`�z�\o���m��z���;�����m��zPeo��m@��
���	U"3.9;��xv�m��L���Y�-\9:h�Q�i[rn%���-9��Ti��s+�M������qk�V������9���6!�&�kts� ���1�"��6_�v����n�M� ���L�P��L�P��L�P��L�P��e�:�����w�����M�
U��)i:;�+��O*^�|��m�/r�m�ic%���y�PM��W�t�y�PM��W�8m�j|o�&1���s(�'�P+
��Y� ��+a��k�O@n�|r���;������[=.9�V��K������sl9m������(���p�$�%*���d0�*]�P�|B�����6�P������N$���������?����'��Z������4������!��?[�D�!���fN�;�-W0v��D���$���'�m�aT��N%�H:�Tr���O%�H:�Tr��i��O�#���sw�9d������L:_>�����E:���1������q���B7@r��M3�U.j@��m���1������Q�z/+������^��{s�\�"RzH/�'i������N�hAU�p/X�^>��A4�e�����7� �����9`(�Y�$J�81`r��*!*�KsI��	ys	a$a�H��R�U4�&�%�o$@���	#I��L�\
 �
��s�T�6�%��9����<��(w�A�%����OB&:!����G�D�
�
&A�0�T��!s"��He��*�:CqG"wj0�7k4.���x*5&�3(�.~%���^��B�[6;q��-�Ae�V4�"0��4���E(��E����P5P6���<@`���,t{���[�v�Z�����r�q1��n�t�
)s���M��]p�3,b�x�Dt�:��g���ZT��t�D*�9H�P6�n��`��b����[�)��`�2Nb���a�>���-c�p2��P�1���V�����Y?W����Z���������@c�F7���Z������2;�8�����s@�N ]��c�&����V�M�G�����?��/��i&�6�bX�D��
����f��>���(�.
���e.�8.� ������0E�'o1�Q��aX-gX�������O?��v�`�'�������5PD���[����v`��'�{�27��Qyb����x;�����o��N�WJ/��l1���M��������`�[�'���d����Iyl��I`e�)�<l�BR �����E��Ef�<�c�*�r�A.�LD��R�PC�V6�tS�dL��������j��O����M=�e9���g0�X�a��;��tX�)g��_�'7�&Pi�6����k����V+n�%m�J�T��z6�F]���o������?Qd��j%���
b1������]����z����������8[�N�*���J�N�S�'�`3��GI��5`��p����C�x2}��Z����ud1��hTH���M��).�r�F�L�l"���M�,���K�t>���B\�4\y��L���t�������
4���f�7'���3�N��i5�@/M��;������r�=��S�3{�VS�R�o44����[��v*�M7�7��q���(��f��Yd�����"o���� 6�h���L�e�����R`���N����kXV��0[���t�@l�����q�r(Ifc��s��\�=��Cp���0��4�R�;����>�
�[�����&m�*������=��L<��$����l�����t�����lLh^Q`��FE'����z`6>P4/01d�t�d�����%���S
4������mFQo�-e�(S�.����3��tt�����V>==:����z>�-V���U4���
�.�B�&������R���e�TO�N�0������dr��O����hU����������*j�3�f,$=O/g�&Oq�o5a������x�����)��B1�u\n~������z���6S�;�M���*���%!'^~�sm���3lX��w;�0}o�:��!�����T#�������f;�]���8�@��q<�����e6*�7�=��gDE��P�*DE�&��������*z��h���"�>.��w�
V��F�>z�wE�{��`g�gB�$h�
FZ>J�Rf.JV���bP��np����.>�������9Yw�	U��N��g#��
E��P��0Q,p�5�����<~g',�+�J��En��H!����F�"|�P��|&|p�������������3!DJi�
FZJ��P��D1]:�N�R$�Am���>k�7�O��U��|L��3!��~G�hE()������P�o�#�4%���M�N?�P{4w�=d>�����2������,�W������G��j/�����x��G�hQ`5v���t���Cp9����l���<
����x��v�"tE<���,�C����Qo�a�A����g�����������;t��#lc��p6	�+��NO"x���O&�Y�q� <��G�D��V�mE��Hq����F��@�b���cX	���6������^� �cj��w��B�P
�p� ���J�}�f������dk�����!�, L����*C�OAU�G�/|W�|%��v-����oAUQ�����O��������������_�������,u��������j�K'|�Oi!?�������u���:_���Uu��������U7��R�����>SA�r�^�A���=��]OG�x���zF��}��<
�Z���|��o4���������cU���/��xj���jj,=[ac7�m��E�-�:@X� �g�3���^{���	�����[�p?j�7�+c2���8[ao�,�NG[��V����D����o��{�K0��n������G����
�?�3��~Y�����b��YV����@1uu��ce�U�rG4
	�����������>o�j�So6j����},��Ej�Ib�b�c�����Y��R�^���U�7�Q��0��G��W��>�_U{�(/NO^��wapr���4x���6~�<zutg�b0�hZ�]�>���3E��o^��Z@Tp��p��,�����������P�K/�^c�������������:�[�L��ij^g6�8j����Y����/��;�:�g��:���XQ� ^�KFBp�����o�()P�G��D��G,�'�>���G� �A�Z���
���y���k~���p:�����42�,�O�t0S��rLh*�C�C*��^����'���F�|v"%<����������B�gCH��&1�~r�|���r[jO�KV�pE���������2�b�r`Fu�tq0c>F>Lm cf[����)3$%����@����%Tf�5 3�%%��f������ESwJ��y���B����L5Y��� CA�p?y4H�������[��:��o��SR_a����w
T1O��\�Tg��z��a��R�=��VK��)|������[����Q�	���7uu��������F=P��5����=���)���n����6}Qx�7�6~��=ZQ7l5���u;���Z�vH����������B���������Cu*��l����h���O%Q.�K�������V�ozXm������������p��Rr$�@�������6�F���Pl}�)�m��{���������\q���^����p������o�.#
���[��`.Qj8�W'EC���o��G�;�&�����Ry�f������a�h]��6���5�G�Z������Ky��_0��h����q�t(�R�{�=J7����J��kL6V>��X6�1��"�7f9<'������	��F��>7���|�:����p<bT�6��������O��w���
F":���(a^d�>��v����>�G������;�l�[j2a���;��o�����T\K���N���{AcO�w�9?���Y��K������_K�?�����l��f�e����{����6���7����
��}1�-5�P�K{�M_��~[R��R�4J7�����W��:����m������u���vv�{a����(�������
�sI���C�������-�W��?X�2��Q�;)�?�{��N���\��!# ��(�Gg�_p|rn��(�p��V�Q�M�P��p]R�/`G���#��U�
������
��g/NUG),�>G��������q���%��~�-����l=]�]��'����]0��&��D�/���wG���'��"8:f{~�&�zV������6�3��0@s�g`���T�V���}����������g������k���R��?�l���qH�=~;z,v��K�p����P!l�?	�#�(�b|�R��bWOi�K�b0�"������\�������������c=��z�S��O�z9�^�w4�C����?�������?
����O����?�_C~��4��?Y��	V�,�Z����9?�������?�~y`���]����@V5��������RT�:.N�����c��������Oq���b���=�z0��2�hOe�X�LB�#��1�������PN���N���5���l\����GXG,Bg�|�\�$�~�Q`��;:�R��C&���D���Y���8���&���������<T��[u:��"�%���RA]Bcb�
74�I<�Z��j*��5�BM�zxd�>88~N�7���G��������m���#�A��.�������

�1=�U���1���k���[�$�����[8��/��._�������
T����gk�l��o����l�/[9+�����[�������j�#���G�����g���I����y{�d�������bk_b�,�4KL���y�/�%���#��4�P�z�#4���a.���G�WIq���ar!���_|�G{}�h��P��P�vp��p�b^�a�#�������o19��[N�z7��f� "(��T���0X�>��u�����x����c�cx�s�7�I�5������!� ����y�?!����"��U{
����iL��f#&����Z��0�x���� b���&qH�.���kY�V\�f�)/��~��N/�^�q�j�w����E�@rb
�����"���k84�\�EnL�;�����=F~10��`2��2E�a�}�sm�?�[C�����3	�����E�����+����z_����}�^__��6z�j��dB���\(H�_bv �x^|���F����������������zI��?c�X����2ebH�T�Fwzx���a����L�T� <�k�����*��k[�*)����_�-�);6�����tY�Jg5	.�8%���A����3����~���)V��f��U��`4�����:?�KT�W��n?�V����t���'���i�/=�{'���78hn��N���&bY�p�>��t�t�e0������iOn�A��z�f�E<!ANG���g;�����������.||��_��d�����"�JQLX��|�~Lg��E���a��v
1��o�y���
,J����$�H��pJ6A�Bp
���m�3Y_O)�]��T�Z>Y5�rb��>;9;?�A�9�l�m>	�O��Hi���9<�G�����+#f�Ykh8������)�u�
�����o�r���C�aM�Z{�-`K����Y+�~��U��������������LV�BM~ �����3p��E2�'K�*����RRb��������������#���K~��H�M6���^��
����
��e<Q[j���3�a����������|2�u�:���
,��opN*�|�����=eX����a����KD�j_��Wp�.���|1~?��WV?^]��%q�����b�(��+�����XI�x<,b4�~<]�����/�
~���qN!���e_M3����~+�m���/�#�W����r�a������u��y�d�?�����6������2^�0�^���j��6����=(�0>����7��-=(��X���y?���]|T�R��b���l�`��m�Ls��L�Q��_'v����:�D�R���ak8����S�a��7��9��a9��N����?��>����DY���y��^���D������������������������
�>9�>9�>9�>9^��}r�}r�}r|���'��'r���?������������w�����������'O��E��}t�}��(4�>.��#.�I��]n����>������q����������������}�E�g���Dy��4����(��3�?m���$��Q�����l�?F���NUU�����<��2 ��e\
brin-results.odsapplication/vnd.oasis.opendocument.spreadsheet; name=brin-results.odsDownload
minmax-multi-queries.pngimage/png; name=minmax-multi-queries.pngDownload
bloom-queries.pngimage/png; name=bloom-queries.pngDownload
�PNG


IHDR�h[���	pHYs��Y;~IDATx���{\��?��u!�vR�,S���x	�4�3S)AQ�v��r�$Z}=�&�1�[�Q#����� x������(�.xcAvo��o�;���g������g>����{g^3�������	���``��!f� ��``��!f� ��``��!f� ��``��!f� ��QZ���/������J�x��8p�������{�}��W��w�q����/]���Mw��������ok����3lh�~����o�==3��(��y��}������Qv`�F�j��%��k������q���C��j���������������k�<��0����8[��P��-���s�/����'��z��W�~���>���3fP�����~�I���>���`�
�}���\������~����R�=����V��e�;v?~�����u����u���I�&���#G&N�HS�N-//��'�}�u�6z�h:����}2
�LKLL���+W�>���t��mqqq7o��j�����:/^����C���g�������z���E�&O�\�����;�i��V{BB��<==
t��I�c����v�����tAF}:�S����5������;�����9����s�j�����x�
ZK�m��==���P���9<���=}���c����8��3g�q������
��9s(�^z��������={v�������������7����_�p!88x���M�6���/��6m������$�	�3����.g�����(q�z�o��4��6�5�qV���tLy����n>|��TT����)��9����� ��@A0�(�~������S���������/S�����B�Mk��(  �����***JKKsss� N9D�C1F!�������;M�����OQJ����G(��L�R��KJJF�Es���M��Ds������(�RSS�,Y������4iR�����k�������q(������}������q����~��#F|��w4����}�QVd��[�65]_�v�K�.{���.TVVRX.^������'Pg$===//���e���7R0��
�C	7=:���	�<���*h�r���2�b�<�H�N��������mU�C0�����Z�n=l����Lc0S�u-��t=MW]7n�0m�����������2���']n�A��w����'��j��kS�!Z2d%���	Sl�����A7)Eh~tYI�pt�I3�kA
$��
M����������>�ai��_��������f��QZ�Nun:~+�Y�R�|��)�)���������{��R��O����N��<""���m�Z�T0�N�<I�Ms�I�U2?���S;{��H�C0�����lO�^�J�a�Ik����F~����~�oS�T���;�����(��6U�."�������0]����s���7�������������7�0a�����RZo������V?�pX�VKYKw��������D����L7���������s�f'Pg��M�6�57]�w���.�E��)����G}||����������&D��u�V}kX�`�]�~�m����0�juII��&����i�����wo�~�(�j�?��4�M�R�.�)](B���P�'�������~��%�����������Eg�N�L�2�x�.(�{,�BOki����i���=G����N��{�.=�:ghv�ux�Fee��U�F�M���mh��z-����o�������������)��{"��-[�4�|�������];w�\��m��	��C��6m��"���(555))����6m���u����e���S�?N����]�S(8p��&''��D�����x�����r��.��g�'�|����o�[||��c�����Xf����=�P�&M���n�|����%E �C��Mgv�t���.s���f^��N��������-k��m�g�{���N�L�W��Z�?����~K����f�4~���	&�}��`F��������]q>|���/^��RPm��i��Y������:t(��6�W��`�����O?��Fk���5�<]�Ql�i�f��1}���9��?����o���;�1�������������~���#G�>V�.]�����6l� 
[�c	�5j�(����;�k��?�����k��q�
4o�<��"���V���q������������n�������]"���S��:�!M�8������s�/�t������ty��6���*##�6��)S����.���� �O�����������_�;v�h������e����m����O<�DEEE����������2�V)��?kI��t�������;z�(]5�},�:�8�J���
c{DD��+ha��u�������2��!]���9�B����@�L�����
k
eD��f���o����t7-.�l~��W��R�83�]<x0%%%55���c���4t�����h��9s�8z.�8y�$]����O���f��L�b�gd�������C��o�v���t�)��gf�8qb����<N`�@��>]��5d��=�T���{�����+����)X��3C�A00��3C��_0gddDEE���������cbb���t:]||�����j1>���,�S0WTT$''�_V�KMM5�������999r���^Rg�)�������ol9x� ��z�F�R�����m�C;��ac.++sww��=<<�j��b�ZI�333�_>`�6m�������:,�===����e�V�YC�Ia�h\��ukDD��O
�����`� LII���,,,��t^^^r������R�ZmIgG=}�:�)����ccc/^���i���������p�TYY���@}�j�z�j@@@~~�%��b�`���������b�
�M�B!K�F��T��3S�#A00��3C�A00��3C�A00��3C����q���y�?���.���`��
-��f���O���=&�	��3H��UU�u���s7{�@Tq!�V��>���fnv��E� ����`��k����������<���.�`Ouw)�L��X�`pr������F08
3C�A00����^�N�,���y����5�7GsFFFTTT\\\tt4����111yyy:�.>>~������������k�btw�5p�sEEErrrdd��%55�`0���Sd���X��P(,���f77�������[<B�F�RY����m����,`�o�eee�������GY
+Z�Y+�sff&�4Nf���
�T��0�^)����<[ ����9�k�D:h�v�z=f��!���4�v��]��Of��[�s}v��Y������5������W�e�e�f�f�m��j�f"�T��H��&M
Md�S���������_�j��5�h�4��s``�q�R9"�����Z��������>�b=>���o�0�]��B��L���`N�����+�j3]�b�K,v/�
��w'�G,Tt�[k�KXX��O����9.�������[��
f:����DFF�t:///�Z�z}ii�Z������4�_8,����ccc/^���i���������p�TYY���@}�k�z�j@@@~~�%����`���������b�
�M�BaE�F��T��3p��/������>�d�����1*ns���:T��5@0���3C�A00��3C�A00��3C�A00��3C�A00��3C�A00��`��������������z�>&&&//O�����<���h�:���`���HNN���4����������b
���KZ
��w7v`C�������0�|c���CBBhA���T���"KZ���-���3
fSeee�������GY
�-����933�nz���
����0�^)����<[ �������g�������!�\���wW������D-:Bnnn>*b�[�s}v��Y������5��q\NN�y%*b�e�f�f�m��j�f"�k�<EG8q����$N�^L���gyy9���j=k�m�twc���@�2�rD���]c�\�U����%}��z����[�������n��.����Z�7C���y��]	*b�kW��f�2��M����B���	*b��s�Z3]���8��b=�qW����?�?�/�����`�C|JJJdddaa�N������E�������jK:;�)�C����{����M�&%%�����������������cI���W���-�������/++�V��+�7
���FC�lag�0��``��!f� ��U:���4�g�,�yK��������Jn�gf��x����!f� ��`��)}��;b>N�:w��lDi�r������:���]f�� ����9���X�J���`Ne��n3}�E�i��``��!fh��[�bB�s�j{���B0Cc��cN���+���``��!f� ��``��!f���QQQqqq���tS���������t���������+�3����������HcKjj��`HOO/..�X���1mQ(Vt`��������0�|c���CBBhA���T���"�ooo+:���`6UVV����/{xx����b�ZI�X�d����Y^^�/k�Z��Z�����I9m��u�VygZ}W|[geei��,��6�az�R���-y�@��AA;����;+��7n��������S�z63D���{��
��"z���&xnnn~W���N��>�v���x�A�a;�[�s}v��Y������5��q\NN�ye�V��Fi��>��o����A�a�e�f�L�m��j�f"�k�<EG8q���*�^�N����)))������:������E�������jK:G6.S*GD�=�I����������?��S�m���������>�b=Nn�N����y�G��oj)������ �s����n��.����Z�7C���y��]ICW��
.�L�{�j�i�i���n��.C��<D�w�r�Pl}w��).������to�i�����L���0N�#mwEl���S�C�R[�����������/6m�4)))---<<�~����2!��~l�r��������|K:�vn���-���3�?��k6���`���������b�
�M�BQ�E��P*����dp���sS���������f���p���=��Eb�po.��l@��pKf�uh��-J��d��q�;Z���a{M$C0[��-��Y�w�5��!��``��������b�xq1��k6rB0�*.���S�G0��B00��3C�A00��3C�A00��3C�A00���������������h����cbb���t:]||�����=MW�t`��sEEErrrdd��%55�`0���S����(��V�t`�����[BB�����-		��F�R��������[%��N���������e�i�Z�U"�X������Y^^�/k�Z�)�J�sff&������[���?O<+:�;w����Sh�]�m�������� ,��+E;���gD:����������m����#��y3�\E�
���1���_~��� 0D���{��
��"z���EG�������Of?%:�����#�T�	����K��s����6�\�;wV��D:��vM�`���s^)�=;�<�Mt�����D+�v�4Xt�}�������r�e�f�L�m��j�f"�k�<EG8q���*�^�N����)))������:����K�������j�U�-�q��������f�����y�����J�J'������x��a���[oK����8��;%����q������]�Je�"K)���q��n0�%f�sj��e&r���w%��Sy��%��U+O3�}�K[O�����L'��v�[m���!C8�����+[���o����}b�5���pk�����P����NWt�[k�KXX'~�����"����)����[�]�p�`�������x�b��M����������'C+++���W�^
���7]e��'f??����Z�+V���h4����P(j�2m`��3��
��3C�A00��+�����I�&���:tpqA����L�2e�o����|��1�Z���(��'k�������7##c��}�&M�{J��5�|���5k�����Z���=%����`~��ww��1w���G�N�<Y�94Z���h
Zh��]rr2-����@�$-�
�R���eK��];v�x�������S�6��i��L�_~�e���|�� #k������i���~���0//O�94Z��7�|��������C���sh�����Gy�m����}��3f�:%����`���9r�J��}6��5�����N�6M���5�����;�]�v����XY��xY����M�4�}*`M0����^������G������`�0~��W(����cbb6o�,��$���������������z�^^^�N����<x�i���P'k��M�6c������{o��]�)ISQQ���i�*55�`0���S�����(
���>�sII����;u�������>'I������o�����!!!���hT*UQQ�i�����gP?k�y���+W���wo����Y#���B'
�������GY�Z-f`�5������1�M�6�.]j����s����gyy9���j=k�j1v���^�o����������t���=��u
��+������\���z�h�{[�l�H���v�#�=�wVt{��;�St��7o���H���3�����O���s=v��]��Z�C���������������DG�~]{@t��;��#�]�v�Z<`����������c��*7�h�{��X�8.''��Rl{v<y�����%?�V��Fi��\�'?�umn����(+6�f�m��U�4��_[�):���T����&�_�����Q�F������'11Q���B�������������������B�N���e�b��2.S*GDwG�q�{�������V�pU:������|���
��\���{[��_���-�)��>>���o����h��T*�YJ�&�>((��,~pc���-1�%88�S��f(3�;/��+���k�.���Zy����\��zr��P�4f:9��+�j3]��y�&���\����D|{����[����T���3�����P����NWt�[k�KXX'~�����"����)����[�]��&�o��M�L/������[������/^l��iRRRZZZEEE@@@~~>�=�M:�VVV&$���M[�be0o���C�yyy�n��}N����eee	[<<<(�iA�P�X�B����)V��k����������GY��A�T0���;��	�����_c����w/e��3RSSe�@�dM0���1c�P0w��A�G`#k���������r����k�Z��Sh��	�����+Wzyy]�|�����{J��5���u�9s�4i�$??���C�94Z���)S�x�����c����j�>��yY�UUU�������4i��Sh��	�����Y��W�^��x����	�w�}w��s��=z�����e�@�%-��;��hN�:��C�����@�%-�
5�z}�������m��5�<���
�B�T6�T@�shh����b*��10DZ0����~FGG7�d��3�;�U6��T��or�Ub\[p�\�5+Y��qqq���
���y��aY��h�o��G�u���%b��������:���9�^���5�|���#G��>0#k+���X��\�A��
p���\�%��|�%+��s�����m����'�xB�)A=m��+�����v��r.k�X�Is�N��w�H��[����fw4���,���n���_W�	�����G�����7���N#�'���bZ���V�k6���/��b�pKw�k6�q�����X��P�=}��Y�`��S������W�.��`>w�������z��+������111yyy:�.>>~�����QQQqqq�;��;���O�{�����Zi�5�aM0?��sM���/����!==�����6''��v���������HK:�����r���2.|��f�4�	��^{M�yX����!!!���hT*UQQ���7����-!!a����t`�5�����2www~����n�d�H���L�i��u�V��B_�2m��
�]A�:�D-Q�:��+4�qY�C��+��t�vi�������O�����;�=}�p�@�C�.C������'b������p��|��s"�������H�H3OD�4�DDG U��bq���xe�H��.���������O�`0�D��p���j�o���+"���������+���?��f�H�k��b_u_m0�D8��'�����"{��<�A1�SD:T�5�D�z�����M��H�����b�t������j9�fOO���r~Y���M�:�)�#""l���#02
<���'��4�DX�;O���:}0S����DFF�t:///�^_ZZ�V�-�l�	�p�`OKK

���LHH���W����ggg���^�x�i��III���3S�>�
��+�-��R�������j����)N��3C������E�..��?�������C��o����/^����a
*�T�Bf�C/�^x���?������===_yE�7�t�����s��-�F����k��6j���={���[QQ��+�0s`*�T�5��$f��~��I�&egg�rttthh�/q��?�?�:~~~{������C��M����kP��"� ���V�����_���53~��$���k���-�����>}zxx��y�hO����<Y���b�@EX������|�U5s�LWW���dz��i���A���&L���_�-Z�-��-�4��3gV�X���K�tJg���#P��"�AE$A0;����'$$���������?~������O?����F�i\�����>k�90a
*�TD�4999�7o>}���;wz�����_|�y����<���o���u�o��s'�����fff
��.i���
:>y���`����o�7n����0���a����P#TD1BE���YfKi��������#F��(/_����������>z���0���7�k�N�R��X~/S�����Gizeggg�l�R�6�$�H-��*�CEjqxE,�`��?�0c���}�
�������{�Y��}���o4��	���@���A�v����U����W_}U��,m�IP�Z���6m�t��q�i���"�����+b!��&N��/:T�:|����'�����K:��{��B������<��C,��bG8p�������g��;w����O��:wT�*B�\�����K#G�,++{��7SRRP�#�����AG�T�t�9'��CBB��?��u����C���6~�x�_�tM������+Wh�V��:�wTD������K.�����AWUUURG@E������Y�:�����-���?���my����_|���jz������������q�����������n�y��}���b�"t�9s�<����Mh2*���!iTD����QK �%���c�\��;u�4o�<�wz����y����(���3g���������Bk���]b��Qt6m���hG�����'/^�X��<9T��X�4������}��G=z��:*"�}D����%��~��}��K�Y�f}��w����?������g�����������w��]�~��#xzz�~b�g����>}��1Z

�6mZzz��P!*RXX�e���k�����.Gh�J:x�"B�G����X�,���G�j�s��n����V�}�����5���F%�@���������g��M�J�*"�BE�}��1c���|���i�9r$22��e����a��>��%��~��}�����wo///�f||�������_=r�H:g��Cp�
6e����(z�>|�u��RG@E�X�]�<��3z��e�����}��MNN�4*"�}D^,T�f	l?��>�un���c��\�<~��+>��v4����'�|�����i��s�W�^/����P!*2y�d:��A|��i��5�WWWI#�"B�G��BE,�`������#������u3�{�_�m:t;vl�>}����G#�t���c��}��v���g���F@E�X�H���R*"�}D^,T�f	L:����9�������j�Z��jh����x��:e����w�������s��ysVV��a�f����g����;$����PS��TD*�M�<T$����5k������T*�����_
������:��e��EEEqqq��8q��0w+�9sf���			OLL�}V��-#�"�BE�PI��^��V����%�p��Q��r�����O�:���$,,L�K|����g��?*�^��
r�K���_y��s��	[$���K���_����L[R��J�*"���"F����Y��>�,22R�k�>�����_@�Zm��m��m����(WW���;[1�������i��#��Mg��=�\����o�>�|�#�"B��*b���PK �%X�vmrrr||���
#i������X:���aC�^�����T����!55u�����������?��]�v��
��R������CCC�BE8T�*"�BE,�`�`���{���X�B�?�������t��n�:����}{��������J�D:yyyM�0�fb<}���F��h�"���4i�����\BE�P1���PK ����meR?������+����sAF����Omy����io�5kV��-�9�u���J1BE8T�.��#1��������KKK333��R?�g��-C�}������O>�D�Y6&�kP�8WE� Y�v�T*U���l��g�y��7&N�H������n��k��
*�T�5�U3H��b������3~�)=z��q�����;v���S�N������q@EX�����*�`�^III��=����M{���2�����\�������h���E��"*��0a
����zT�����n�����Q#8���lZ^^��/�8q���~��};
��+��2%T����IQQQ�V�PF*��>�ZE��`��i]��y���Gp8���t����&M����������PcqxE��[WUU����:T��_�vm������GE��#��TED �%���c��a�7�j�Z�%��Y�Z��a	TD���,_�<==}���o����/�L��
BE��0���X�,���G`���l>s�LWW������\��$AE�X�����z��G��J�������y�PK �%���c�����M�w����@�������{��]b�"���Tc��Yt�u�����0I#�"B�G��BE,�`������#���o65g���7o��_9���P�3f���g�������-Z���OK�>"/*b	��tl�����_9���P����s@E�������X�,
����j��!#�7d��qTDF2~	�q{
�p�B��������@0K0k���K����8P�T���_~)��m�~b�W��"�BEX������X�,���G����+�O�>u�������0I/P�G���������P��"���"�@0KVQQ��GZP���/������r�*�T�5rUD�Y������X�V�a��^�z%%%�T*;�B�kP��"��,��?�����S�����u��<�"�AEX��8#���_m���7n��� ���a
*���l����w����JHH�������eee�sBEX���qFf	-Zt���;w�����K�,����m�P��"�AE��Y������\�q��_|�~�BEX���qFf	:w���;�����O��m��SPP`�@a��7���������APyaqFfK��e��5�w��o���n�����m:��B�k���x����z��i����������gvTD^�G���R��o��;���V����,?��>�"�y��������~���O<����[qq��?����g��?QyaqRfK�����F#<���������#�*���^zi����������+**h�~�����'�`�����,#�*� �B���uwGE��}�!�����#� ���Pyaq:f� �@N7n�X�`����n0�����N	�� ���������P��]��M�6�F�R*���������A0Kc�A��@a�����O?���g������8�4�tl�P��1b��������7�[����*"/�#N�,���G!T�5_����#���
�u#�"��>�t���~��}BEX������OZ=*"/�#N�,���G!T�5:t;vl�>}�WW��-�4*"/�#N�,���G!T�5�������\Z�"��>�t���~��}BEX���-#�"��>�t�����W���u���vq��m�P6�"��>�t�}�)//��.]��R��e�d���#G�yBEX���q:fi��_?a����'s5��3b��/P�G!T�5�kP��`�F��0�_vww��G��B�kP��"N�,Mhh��)S��������c��� ���a
*�t�������W�6m����������?�"�AEX��8�d�m���-�N�}BEX���q.�������AEX���q:fi��F��"�AEX��8�4x#kP��"�AE��Y���5�kP��"N�,
���T�;jp5_F4}�tZh����q���������A0[�x��)��'O���{�/�� ����}��}����9m�4+F@E��}�I!�-e<�8pBEXc��J�"��>����t�\�r��oo�f���sBEX������B0KSXX���Z�lYTT7s�L;�B�kP��"N�,����g��=q�DZ����A���@m�P��"�AE��Y��m�zxx������;w���R��q�A,��r
RT�Q��q� �AE5�������h0RSS7o��������_��6���DF�kS��c1��8vS��c���R�w'����o	�����P��"�AE���R��+��#�*�T�5���B0�rss

����@m(��C�����J��MKz�t������{
=%��W�YX�s�$���pgA0�n��YZZJ��EEE�/_���_~�}������~��u�����q�<<<,X�����k��#F�p����J�B��t#���\�r%88�`0,[�l��Y����K/^,++�4iRjj����������k�)����3g�z����
�FKKK����L�P8��:=�.Pk�S;mgZ������;v���,��D>8g��u����������}���.<{����#�����[����?�����rcQ_iP��������td��y�����������qcJJ��������;w~��''N����]�}���4��?O������7�t��q0=L=�����T��v�������� ��t��_��={�����N?���]]]7m�D���.//����R����62��A!dg������O�����j������>3�S�J%
����S�S�w�}G�=x�`���7����W�]`��U�6�����a�BBBF����S\\��v�}�E����0`��1chy������=a��A}Y��=��7�j�_����:��V�Z�u����y��|�N����?������;zv��`��������������\�z��3�����P������_��:uJ�g�{�]�9r��W_���_����_e���L7�������:4e��}��q�ogA0�����g����������'S�����?�����;zv�
a�����-���������i���v��cGHH���U�l���qx��wjm|�)bbb(����>���9s�4���|?]����\�p�~6k���o���h4yyy��a�U_iP�
��/����#==�o���������3��E���{+��w����4�]���_�d	�@��/4����3C�A00��3C�A00��3C�A00��3C�A00��3C�A00��3C�A00�\���GO�a
*�T�5�T��3C�A00��3C�A00��3C�A00��3C������IEND�B`�
bloom.sqlapplication/sql; name=bloom.sqlDownload
minmax-multi.sqlapplication/sql; name=minmax-multi.sqlDownload
#25Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#24)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

Attached is rebased version of this BRIN patch series, fixing mostly the
breakage due to 372728b0 (aka initial-catalog-data format changes). As
2018-07 CF is meant for almost-ready patches, this is more a 2018-09
material. But perhaps someone would like to take a look - and I'd have
to fix it anyway ...

At the pgcon dev meeting I suggested that long-running patches should
have a "summary" post once in a while, so that reviewers don't have to
reread the whole thread and follow all the various discussions. So let
me start with this thread, although it's not a particularly long or
complex one, nor does it have a long discussion. But anyway ...

The patches introduce two new BRIN opclasses - minmax-multi and bloom.

minmax-multi
============

minmax-multi is a variant of the current minmax opclass that handles
cases where the plain minmax opclass degrades due to outlier values.

Imagine almost perfectly correlated data (say, timestamps in a log
table) - that works great with regular minmax indexes. But if you go and
delete a bunch of historical messages (for whatever reason), new rows
with new timestamps will be routed to the empty space and the minmax
indexes will degrade because the ranges will get much "wider" due to the
new values.

The minmax-multi indexes deal with that by maintaining not a single
minmax range, but several of them. That allows tracking the outlier
values separately, without constructing one wide minmax range.

Consider this artificial example:

create table t (a bigint, b int);

alter t set (fillfactor=95);

insert into t select i + 1000*random(), i+1000*random()
from generate_series(1,100000000) s(i);

update t set a = 1, b = 1 where random() < 0.001;
update t set a = 100000000, b = 100000000 where random() < 0.001;

Now if you create a regular minmax index, it's going to perform
terribly, because pretty much every minmax range is [1,100000000] thanks
to the update of 0.1% of rows.

create index on t using brin (a);

explain analyze select * from t
where a between 1923300::int and 1923600::int;

QUERY PLAN
-----------------------------------------------------------------
Bitmap Heap Scan on t (cost=75.11..75884.45 rows=319 width=12)
(actual time=948.906..101739.892 rows=308 loops=1)
Recheck Cond: ((a >= 1923300) AND (a <= 1923600))
Rows Removed by Index Recheck: 99999692
Heap Blocks: lossy=568182
-> Bitmap Index Scan on t_a_idx (cost=0.00..75.03 rows=22587
width=0) (actual time=89.357..89.357 rows=5681920 loops=1)
Index Cond: ((a >= 1923300) AND (a <= 1923600))
Planning Time: 2.161 ms
Execution Time: 101740.776 ms
(8 rows)

But with the minmax-multi opclass, this is not an issue:

create index on t using brin (a int8_minmax_multi_ops);

QUERY PLAN
-------------------------------------------------------------------
Bitmap Heap Scan on t (cost=1067.11..76876.45 rows=319 width=12)
(actual time=38.906..49.763 rows=308 loops=1)
Recheck Cond: ((a >= 1923300) AND (a <= 1923600))
Rows Removed by Index Recheck: 22220
Heap Blocks: lossy=128
-> Bitmap Index Scan on t_a_idx (cost=0.00..1067.03 rows=22587
width=0) (actual time=28.069..28.069 rows=1280 loops=1)
Index Cond: ((a >= 1923300) AND (a <= 1923600))
Planning Time: 1.715 ms
Execution Time: 50.866 ms
(8 rows)

Which is clearly a big improvement.

Doing this required some changes to how BRIN evaluates conditions on
page ranges. With a single minmax range it was enough to evaluate them
one by one, but minmax-multi needs to see all of them at once (to match
them against the partial ranges).

Most of the complexity is in building the summary, particularly picking
which values (partial ranges) to merge. The max number of values in the
summary is specified as values_per_range index reloption, and by default
it's set to 64, so there can be either 64 points or 32 intervals or some
combination of those.

I've been thinking about some automated way to tune this (either
globally or for each page range independently), but so far I have not
been very successful. The challenge is that making good decisions
requires global information about values in the column (e.g. global
minimum and maximum). I think the reloption with 64 as a default is a
good enough solution for now.

Perhaps the stats from pg_statistic would be useful for improving this
in the future, but I'm not sure.

bloom
=====

As the name suggests, this opclass uses bloom filter for the summary.
Compared to the minmax-multi it's a bit more experimental idea, but I
believe the foundations are safe.

Using bloom filter means that the index can only support equalities, but
for many use cases that's an acceptable limitation - UUID, IP addresses,
... (various identifiers in general).

Of course, how to size the bloom filter? It's worth noting the false
positive rate of the filter is essentially the fraction of a table that
will be scanned every time.

Similarly to the minmax-multi, parameters for computing optimal filter
size are set as reloptions (false_positive_rate, n_distinct_per_range)
with some reasonable defaults (1% false positive rate and distinct
values 10% of maximum heap tuples in a page range).

Note: When building the filter, we don't compute the hashes from the
original values, but we first use the type-specific hash function (the
same we'd use for hash indexes or hash joins) and then use the hash a as
an input for the bloom filter. This generally works fine, but if "our"
hash function generates a lot of collisions, it increases false positive
ratio of the whole filter. I'm not aware of a case where this would be
an issue, though.

What further complicates sizing of the bloom filter is available space -
the whole bloom filter needs to fit onto an 8kB page, and "full" bloom
filters with about 1/2 the bits set are pretty non-compressible. So
there's maybe ~8000 bytes for the bitmap. So for columns with many
distinct values, it may be necessary to make the page range smaller, to
reduce the number of distinct values in it.

And of course it requires good ndistinct estimates, not just for the
column as a whole, but for a single page range (because that's what
matters for sizing the bloom filter). Which is not a particularly
reliable estimate, I'm afraid.

So reloptions seem like a sufficient solution, at least for now.

open questions
==============

* I suspect the definition of cross-type opclasses (int2 vs. int8) are
not entirely correct. That probably needs another look.

* The bloom filter now works in two modes - sorted (where in the sorted
mode it stores the hashes directly) and hashed (the usual bloom filter
behavior). The idea is that for ranges with significantly fewer distinct
values, we only store those to save space (instead of allocating the
whole bloom filter with mostly 0 bits).

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzDownload
0002-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzapplication/gzip; name=0002-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzDownload
0003-BRIN-bloom-indexes.patch.gzapplication/gzip; name=0003-BRIN-bloom-indexes.patch.gzDownload
���.[0003-BRIN-bloom-indexes.patch�\{w����;����w���-?�h��&m:��m���w:;;���h[�*JI��~�@RGn�v��3����AJ��$
X����7�����g2�����h�����P��]���m�K3{��zO���zv�5�y����K��(t�S�����2��r�������)��i����Y�}������N86.���	'}�~{|��g6�g'g����@d/t�����q�������b_"L�p���V��K��p���H����'����Y�}�f���9�(��*��$�L�|/�+�'�\�6�d)P��DS/l%,���eR0�K��)������$�B}�K#�F�1�S&F�=�z"������e�j4��:�Rh����	�<�EF��,@Q[�oA2�U���l&`i�i���D	R���zg��M<�/E���0�y��Y�9��!�@�Dp�|���)��T� ����ct� $��P���\Pe���BPQ�S��]OPN��h��l,S�<c>r\���6�,�p���8'
��PCan�"dHG��.t-#��	 J=�F����@������}�S� H�Y�jx�LwQ=y"�l��.�i�x$#aJ�n����X@{%P���L$-��L�`�

(�<�C���8�h��#�>���q
�nD�`��e�,��	�w��n7���A�LkMI	t��?��kEa@����SRw�I�=�1�r`�1OR��` �v��m�*�w��%���X}�{��L
���,� I*�>+!9oM��'�"n �zP�	��V2+}��c�q'��:}:�<Q��Fr0lsG��������$0��V�h�.	`����,�A��m�����*B4�|T�u@u�LZ����>4�e����l��V���4����L��Y�w'`~b��d}{�v��a"�]e�c�)%&26��`�	w�E�v����c�-�+J}1�g;��$$�S����$cR�&����w�������.��Wq@G�M��g�(K`���g=#t
	hP#�i�8��	��f]x<�b�S(ub���pj�����������QC�g�=���
����(��Tv�WH�e�U�(R!S@e����]��p��G��d�Q
�_���Xr���a��@��"���	��r��Zt�DB���];2��/
��ao�<�%z�d���9���Az&�m�@A�����i5��D�`������Y+	��o#�?�F���S���`���1m��F�B�����kO�������C{WK��r%��y5�/X��m�9�c\ys��S3$kRM��[�N��f���J�m��7���m5��}v�eC�a��dq������|��;��jt3�����}�KpIy�5�snU�t���7��.}�o������k��B���)�e��
�0�w+f�*p��B��OHl R���kC�D����wm����T�7@����&"C�M��
i ��#���^��R�`f	��h1����h�q��.���A�?"�F�
D�����H!���D���S��5(
v	��M�+�~���,�(���8�'A���L��6j_��p����|�E�.~H�Xy!H�.bu�����C��f���G)���3H�������}Zk
���~�
,��/�{4����Z��5��u�`���a���k'���q032��
��#6P����&f�zkg��Q1[?�X%v
jj68���1���y=�<Z$��H��[��Z}
{��o�Y �Y(E�HXi����^���Jl����#��}���z�����U/r
Zj�5nb�y��P������R��5��B���]����k�D<V���]�:��#5���xd�7)X����I����Vm�V�\��M������h��n��&�y�H�u������F�M��R/!x�
�!����M
��>G�#e��H�����R����`��M��������c�����Vn��M�,q��&��wa�]^�K+�]{3P��9����w����Gk���&�xe���=�y�H�uA��>J�}���}��(!�+�L�I����;g�4j����%����#���K����n9��N^ ���<��X�	^zS�^�2s&��,��|sjc���gm|���,�����o����7����.�)��p���3��w��(t���P��L������Pd����s��$�"��=��C}vR<v��=�������|�_���R����[�'O���u�|��w�3u���e @S/���{��u���.S�������/QX����Ye1�#/�o��:��Xn����_�4�t6h�_�,EW��$r��5C,n�wj^�f�,t(��OG�+��v5�si�����<���?$w�1���+E��Ht�E�9��L���������+�(��� b��s;�T�[���u.S�+f�U6�����4���~����(�P��!&�,y����$�;v�BD0�����T}FO����H<(C�.�����FB�My����CO�m�2T9-p@�h�ocr�% �1�<�-"#Ng)x<��o���:���2:A�rs����������#��D�Y�������{�\O��sAojZW��������LfRs��:(XS|J�f��y>=1�"��K���&��3�t��r��x��.-���u�:v(E�#�;'�����?�4��F�����{��G|dYBL'�`_�_9)P����@}C��F}��
{���8=�:e�������������:�^��{��������_��o���k�����~<����k���}���I��M�Z���$L�"P�7<�U�%�V^�t�Z����ocsx�8�YN���,�T�K���x(V���^���(�0
;!�>���&1s�5/}t1����;�`��"8��q�5��'�)�H�3%k�\��I�����=s~����:�VY��B�H�f�-%}�����A���_�h(�?0���z�w�^G&�3W�+X�7�W�!�����_�������s(f��3��{�i�IT��A'�dp��p�@���&p�cIf�V���9��{�YP���4��%��h�	�	�z�&�[u��Z�S��enJ������A:6Z��I�o��SXV<-3������;?��S�K16f3N����JWH���ua���!W��p�%W�*��s����]���u>)1m��
choR����
��0������(�� a%���me{�����&zq1=���;�,�������U'�Y����ft���f�o\�G���"�K�sfY]�����O���r�ucXZ3?�@n����^:l��1n�D�4_q��|�X`����>�o���]UI�i��B�X����[��eY����<��C���6zU�������A��^��j�mA��������Y�������}�-hF_����`���c;��-o���W<�XY�n�IQ�$��l����-���3��<����L ��&��a���w0�?����Ke �������������O�g���e���h�TNZ"|/�:o���� ;����/\fvr~�f�
]�Kc�/d�V��2�%��?�d����1��9c
�D�^_ �_�/���4�x!|M�,[�1������w�SI���#��u��5c
�����z��eM����=r��+n�*yz�C���Oz���6�'x����}��	�4�[[gfS�V��fGSo��mckb�>R'���(^$T�����}p������6���0�@���,L�7�~Z���K�8
1S��j�����Bp�%����:{����g/����I�-�m���i���7C�c%�����74��t��-U�a��R%��M��R{��+����������
�zKwKep	��.�����R����+x�r��������/��U�r��.�j��
Q��!,�@5�����TRY�).����,����v�c]�L���1���Na����(a�"H�o�3��v��JJ������M���1X���[['o�������8��������V�I�D)?�
G���*<4������a��m�����i���l���[��Se��1��V��w*R�R�y�?e������5���9_����k�x7��k�X?j�^����S�����0���������-D���r_��JRQ�+�A[;�Y��(�DLq�H?�Y��w��A�^��.���zuz�n�zU��5-3r�1�Z*�E�� kIu�S�7_7_�s'��{B���f-g����N#Ji���[�z
������xR-��D��w�(F8,(�p$�_T���\�,�K*j%��Fa��n)+,Y���y���4w`�9D���-vl�+��>�}Sgk������H���E�1�2Q���\��lj�$S��Vy}j)��+�������Q{=xS��]g1k�+�@{�G��-�,D��%f�O	\�z��jsA����
]S�I�
��mjyN�
��4�G�T�)��7�q�MI�V��Q�����Z���J�S�qB���^�T����f`x7r��[su�ALOW��Y�g����g��E�.n�bg���FxK���{e6���Y���}Cp��i0	1U������^�n�����a	e9nB�G�`���!�B�C5��c]�TT��V�X���^�W<��)DOZe�
�"��?��|1A=�E����{��k�O,v���j�x��V�TO5�9
Z6M}D�,�e�����GZ�}��6Z�������!�U��}�2 u$�
�O�,"�CU;��b�E�$f�
xd(����iGKsqh�43���cU�_�8�tl[���r)@�����(B8�O�������Vh��sGQF���V�l+S%.�fh�a(0}�����"H�[*�W��m,����9���$��J�
i�A��Y"Wd�p"/l���f�un��T��T���F�e&�?`u��H����3|T����@i�9�|�����"����@2~�s?�P�K���{�@����-���-����YL��#HO�Z�P��gH[�~��+��k	*��sM�����O=P��0����N,���>��LU����NN?������W��f8����iFE��,	�n.1��P��n|R[H��n�����%O��j���7��N����/.��=}{rz�;���lS�J��z����WXx+�M��r�����PmN="��^������&�^���A��~g>���i�������l�;��R�.5��ezC��	����xZH�f��E��PK��%V��t�������P6B9O�X>�vEh�z�8���(�u�[���ehQ���������<�X�T@���Z�����Q�������t��R#�s��o�zx�������I������f����Z��_\F�O�=5u�Y����6������c���ni�p����]�/\n���cd�<_2���O���a�D��H{+�p�r&���)Z^�����b�E�8(0$��-�k{��Q�5H1v�zZN�E��[ ������#����K�g�V:�C���e���`�����j�������9m�i��Q���������D��rv|��M��g�O��'��t�}�����X�7���M���	o�����S����������7'��5X�4��96i�V������iI�����gm��0���]���z^G�Ir�{�������m����_WU����� o����t�TWUWWUW��yU�w"I�y�o�S�m��U�
C#�S�~���$���L�s��Wl�n��uq��.��5���D8JJ��~S�U�NIu}3��h���Z{bB�S�W��"�������S��lf�n�������	��pc�nNw2�&�c`��.q!iLtgP����6���DFyU��&�I��Y#`��Gs�`�r����e9�i����0L�g�5	����C���m~b >0���L5j~�+t[*`]l*���h��VVT�P5,"���j�u�U�Z��Hc�Xo,�"��U����X��CW�(�.Q�����Hq7%S����F��_N��������-VHV�	k��9��
�8G�m�
WWh{%�] ��G����nkj�b��	R�$�{�h��i���_��pc��z(a�|�>��D5�

.[�2��n��-�\|7�����E���J~���x���\wonKR3���+)��0���[	�N�9������$�����,$1��dr)�(�k*#y!�h����F�����|�~i����o����m �(L$V�^P�R�7���/�_�7�L�����L����"�'���sslo�O8�����$���c��M�yJ����
X1�-�	)���jO�m�eu�}��Q1���9�	���d�[��
wP�x������8��e$�F�d�l_��O�6���M<�����Q�[�e�n��I�Py.�N }�*l�����RN�^g��`����euKn4}O~x	C�k�#�J�vH������,W��;y�]m�����^��c�7�+��?��� �����)�yx���=*���t�}o��7�g�FPBFs(��_9=��Yb�N����M/��2t�O 3�Ppt��N�p�@Y��][����Y�G_�t
u�C�b�	-;�Sr�����������IPB�FV��D��W���fI����/8��3�D�1�H�Ig�f'�f���Ed��XPZ�6�_#����	82Y���k�����l�z�!s���:�&y����-kI|!���86a���DD�,�L��Qnm�M��/����tb�j�
�f:�O�M#d�c��:��A����&��o���V�6	k�����jq�CMTSP��7����<�y��+�0@�A���N?�,�xF_�V���Mc��# ,(*��z���nC/��|�<f��I_���g��z������<-���@cO�f��G�J*IB�G{�b8yG���'��[!.q<?T�_���<�7��Sw��E��� :`���J�a4V�yo�t��u�C �U�����	c�����d4���`��AD+�t��O��?L�g�,e��Vu�����!U:z%�������
HV��z���|o��O�����Y%|3������`�&����iA�
��������T�\��/`��-Cj;���0��y���i�0lE��	��>�"gI�_��C���XW�eN��a����Ei����wu��K1���h(��
��V�2L�AEb�7V�
��c��'n�pG�9G�[E�(�:�/�"�?�nB~��MY+� ti���yGt�:���].q�cZ�6�����k'����0����0W�s�:��k�{%���[�'Xr�?
���4�/��FJ�$��*�K�5�7I
����N��}�E�rm�'��H�] .�D��d�g]���M�O3���1��*�~P��\��9�N�3O�x�Ll�42�@��}��:�E�����]�8-p�S�<.+sh�n7l>nN~d8���t�
Z�p"����9oF���8��'��/I����������e*X��o��\ g�^���9��hC/�Ls����1�b�H�������.�)8���n#���q~��^Pk{����_*�T^Ec��jW���:�33��&.��V�����]QGA-��HK��}3*��Mt����F�}yKb�L��o�	DR��[{�������Ag��3���$�3���[<��%8:&����g`�`�8o�/'Psgcv���P�$��sG�:�9F��� ��{�1����t���oi��L����R�:I��f���U�o�h,�[P
�e�N���6�~�/��U~������+�Ai�}.6A���v����4�d�����HV�h����������V!E_�@�Iwf#�C�V��"5�����
G���NF��n�#�	���R�0z�c��A��+�q$^@+���f�Y��r�=���t�����Ru����bW3�K�[4�����=aH�|��C�Tj��W���@�c��q�9A�
�W3f��R�� :����6^.!���.S����������i��w'��g%N��X�Y��*��b^C��5������&�|��u�B1c������|6��$*
���(��������2�4
t`�R:?$����aQ�g&�^f�� �Sc�,��q�?�����7����#�c�R��� �@����
��n<�C�Y��"�0v�K���*��(���8_dn����g]���^zc�2T��Y�m�t����l���;N���%� �������C��0�K�P�B-6u�����::�Q����� ]\�Ax@�3�B����?��N����~��`"��x��ps��(A�0l�g2N�U�@����shF��{�_�{��z2�~�t��H��v�H��*�'���#���B�����$��B-8�����%;�,&���-���#v�kW���k|���E���e�!�}3:�zw\E���\��r�G�aP�i��N���]WG�G���?�vR��;� F9
���4$L�����Y	E�'�s�CRQ�IH T4Hi\.���H��������j
��q/�����DeiGB"SjR��1Sr�`���"]�E�R�������%�)�a\�B�u��FcYM0#��vE6�����u���P}��	G��x&b�PR��Af��'eB��37q�,�Q�"Jh
R
$������L��i�
CBe��������	W��?����N�?���r��d�~M���;�������I�2�Hu�����
=3��hf�xx (z+#��
�d��`��G�����~�O�w-iS9�+u����I�=+2�/	=Ir��a�b��Pih@�tz=m�JJ��"�Clb�@^{��%2Rcp@��*���2yb������~��cfv���)\) �X�p�g�����t�����i`�q�`���64mWl-")���A�V
#�Cr���	�u,�(��L�u]�[�������PJ'E�C��������������
�s��6�M�{}t����6n�bu"�Q
qs�+��!�!�U����o1�����
�g	����H��+�~�>SQ^���t"r����������]\5�X����'�`	$r�	
1���qa����l�XDEH��rf�B����KD)�V?��n
m[@����G'��)��'��+�9��H���D�\GdKS�U�:�� ����.|
d���D�
�te"�X������,*M��C�7�k�T3S���U����bjFn4�������cL���S&�������!���w�Y�����wG�b
�@����r�%�n�SR�����M�)K�@$���"�������q��
�k�Y�@z^�p������	�h��^qW_qT�'AR�#�(A�I�5�)Z8�s��2{*Oy���L����44���q����r�4V�����E������6�mqc�Y��h�[����E�s�&�������'A<�cQ�G�C�h��O��9N�'��c�<���hOu�D�������������Q����5����hn�}X@�}��5�	p���a�:�Pg$�TN'��C������$��!�M�&���8�;hpq��7�6_L�����AKM���)�7����C�?�/S����O�������>*[*�m�0}G��E0�z�t
�)h��*��H+3����Y������=>;���I��
��L�7�)y?`�Q�yCi?�TGC2�O�M����C	(M2���P5Q~**�n����b>������(V}z�<���
Eg��h�a;���80\)&R�<�D����wge�x@~{&�B��Oz���b���l+�}(��hG:�j�%�=���m����k�Z��`OT��5%`�s�iPI�[#QYZ�����]d��<����|���0[_8TM�����P��v�j�F�~�	��w����
O��)��v�	lj
�{ns��
��Y��� 3�����{f<�#U�/������n�������9��v�����R[p ����Y�Q�����y�u�o�8��y��*c99�;�3i��V�0����f�0�OEZ�i|�I���~~zw����Z�	�.���q�GNR�	����P_YMh"���(�������c�I�@�HF�����B)�F��p��������+����T�F�]�{�:�Az�9
��p��;�;����$S�
7�V�H��M���A�A�,���K�l����F��.���k>yHp$�
B��x,5*���#����`��9�;v��_"�\����U3'�e����QdY>�
���t#m���1 ���Fk�c��Q�9E��a�e"�w`��I6�X�`3�d%"1\��ff\C��B�9*��(N�@Y&T�����b��s������Y
n��K.����	"�#e����m;$��.�%��q������o��p?�(Z�6�n(��<�D��e�����-S�l3B\�]?#tW���5��l@�9P��NG��[���$��#*Ir�,�$�xJ�I��
0�?�PEzo^�U��������`�|�	�����e�Tq��5�G����!I���0%D_���<����cj%�{��GE	����p�
j���6PR�P�af���f��	`u�o~H
G�8:��c,@2�{��NQe�XFV�P��/.��ga��/������F8���6t
�UI<�Qzx�3@[����[g����P�H�����
sD�	��j�B�U�\UT�;S����+���z�q`�����R���g�����{���r6�N�'�%�Y����^z:\5$c,���m0���u[�O�%m�
�O'�o����`Vw�����B�T����k�+E�h���w[�Z�V���vz��jR�&��������]��%�Mx�|Du(�/����j_T�=��P��K���^*P����[���e��D3O�z�wZP���e��80:/���s	������#W���q3����7m�|0x�K��E�w���L�����G�C?�6|�	P>�|~�	���Q�"�x��3�����������fpQ��Y����*�=���J���k�Ncww��	���f��{�������|��mz_+J�����JBn,XR�y1q��y_�,�a�s!W�.���F,����z���
�/��1���d@������V��{/�9_�)WBc��Tw��y}��������G���h��j�4���9Z�1]�aMz��9z������\��t��_�"��]��
��h��h��N�����r�k��V������PN����l���N/�W�4����|t��["0�;������CI�.`O��}Y0h�������q{�9��s�
������a�Z����8���Hp,�-X���_H���C�@~��&=OF3g8�}8���r�n�~�n4�I�V����^���[o���n���m&�`����_����26��l1�W��)���H�w�+�f�g�1�p����]����(6����t��cR�.z3G �j�7�*�1_�_2[	�����s�$�~q�|��xf���
��\���j�P�Pl�f����|%{���-�H�oB�q,w����4����z������7;�~�Z�3�k$��\��5i{M|�'~����/�F8h�/��7��d�X�f��w�"�h����~Y��e���Y��q���h,��M��p�����a�~|
����3��_Y/�'(y�m/|5�a�m?������>������|�i���>4���BO�������@'��P���W�"��a7��0�
�xh��)$�������9�H��h>���9D�&>����B�2�QD�����Q����G�D�<"���I5�$�q�43�Y�I
;�e2��
�h0���US���G���(��^rP�r���24'�����&rZ�d�r���Mh��/$�a��5�������nX���5��������k��<��Wk������{��"X�����z������lw������MZ���Dm�����j�������yD�����PQSHR�'IC1s�;��P��CQ~kAy=�V9!�����l�Z�j���mZ��QK��1RHGB����94]�~N��p���4���ayj��E2��+t���?���,�-�G81��_���b�b���wCKmi
w��Vq�aB|g�&b�n�0�����b��q3�z����u,��s�h���s"tFY��D=�VZE��:�����=$���.������8g���>�����U��i[0�������DL���M}��9��|���H\o�k�����M���bTn�!s3���E<5>J�M'�MJ�j5��i<�0���x5k���C�2��R������Z�}�p9��G�:��f-�3�^J��u(r�����:3����*�[����S}[-�o�Qxls����e��2�����+�i�9�77E^~�,8�Y����~�8���x���v����`o�������=�|T�/��=�p���#mf7�|V�`�y�������av"�d��B��C�
�=���7���?���r�n��$#!�k{��S�����S6'�G�b���+��%�D����A�6�!U��7��N�	g\����Y5����b~[r��e����~��8�#����A`/�#Y��������U��W}	�hn�!����	���G���������?��f~�^�/0��}R�����?�s����K���pSp����w�%Q�;�*D�E�������T��?�7df����c�B/�YW�C���z����?���g��j��s���R q��|5���k��[s��[����B(��+��ku:ciT
�r��u2t���h_��_���i���h.	�'��?��_`"&�g3�a��Cih�#5��5�,�M��:Q�X��a���#��4��CaDM!yf��
��a�p/e��Rq��|W\<����#u�`��G�Po4�Ln.��tFOj[��p�R{U�n1]%�n���`���B8};��^�<�<����W@���E�%��s����B�!w^P�l#��+����^������u;�n��L��a�j�9���0�~��#"V<Cw|}N>BCg�="����e�>\��+�q�A���7�i����n/h���a������a��`SE=�t�"�tU������h_�j5�:y���%.(]	&�f��
�:`��	�$t��_���$��dQ1b��x��CV�Dj�-'D��*$��@[��XlK��u��Cp`C��:W�����d��2u�2#B�~�5Of��Ee�Ni�B9��fZ��ZNb�	�UH�5����&�����1��������i&
�o��x=�r�"��~U��K�4k�_��u�BI�0�B`�����^��SH|��0���	����
9�>�V"j��R�f���8��8[\��"{���
X��]�Bw�+`Q��i,�o��n�!���(l�����W�����
X���+�^������j�g�����4L�rYG'yk�kk]�,�Y�|U/�s�i��.������[��Xh+�k�m)��.sHl��|v��}��3��m�d�o��}k�d~(T���h��J�@Y��J�3��p������r1��V!C��J�:[Jz�����y���b-`=N���t9���R1�J�^��Tl��#�:G�#l��${F\��J�I���.�Q��U��u����N�EH�����{ 0D�9z8`^3����Z�Cw�kbq��yM,����;�^��hZ�h���TO�\\����~o5���
E,�Z��iVXr����p��]���|�^E8����uC�
Y�nW"/�
��2c�����yqq����(>�N�3u@��c�F��]�H���(8�TA)�@1���i��+"�s�rd�p�H���-�����-.�=x,��"-6��L�S�4�H��W�������S���]����L���%����J����jB���rQo��7�db�(��=�h�#�R�T+����s��"���H�|�k��N���%��A^A��:�[I��:\M��:!\V�X/��5g@}Q�0�A�b�G���Z����������
*�y��w�<(
���p�x����x�����x����\<��x,�}�c@����o�"t�i��_�\�r�������W��"5%_�gI]%�4�\<P;S$mQ4-'[��UH�{t%r�^ ]V�����S56�������_a!������
��YPw���������Y��QU6�:K������x��\��R��:y.4 [����4�f�8�
j��[�m�{�����	���m�c!��jd�0.���s����������������}������N{�[��g���?������<:,I��&I��Z�T�t��-�Y��%i�~�M~������6W�z[�������[3��s�U1���f��Q5��Q�������g	T�
	�F4'hid�]]R���*�.���ub-����b�a^A���!\I���a\M���\V����'�m��_����2��Y��T��u�QT%q����.X=#����P�!�Y�x�-'a��*��@[�<YlKyE��c�}�UA
*_�������W��!���v��;�U�hF��%���k��:��������]^$��U�����V��5��~6����D�*��4\���m�ETw�.��z~��TM�f�iq�t���������F�Um��V�j����5zAu���
��
���Y����y�.t&�(_����"]���y�v�u�&���:��!���<����N^�L.v�������~<N�r��x�V�h����YuM��:�/����������wF�U)��,��n@]L7��

�|dYEM�����L��,(8�,g��,��xiF]�0���r��r�c�Lc\e��P�P-��hU(�|(��DY}u(��PV_e�U���CY}�Y��<(��e���r��Q�����U��	��,Q����������%��j��@Sr����|i)���Na�������T`(�	�q(�E�5�Y��Q�|���%< ��SEV�LA*2�:���j��E8P+B�{�V�8��:W�
��%9��f�q�`���9T�j�k{l�oD���d9�x]Ms���6��
@6����
>�l��B
w&��DB1�ht/�l��"�c�9���c��/��%A��3};�1%Z!�Bcm-:
5�K��H��p;�e�s8��m�=U�^'.��47�.G�D*�����Y��`�8�\5v2�&���fYD�^3o���2j�	R�A+��	�Cu,�����d����@��[ax#a�|�9c����H��KqE��s��7���"LG���T/x�@i.�qH��>�����
[je�X��y����/������vk�7pY��.!+����/$�w���06u����/��Y3�+Q�����'�C}zM>�1�@��2C��O,H�b��j�j�[��Z9��e8W���h�����r~�\@��;B�|p9BT ��3�x4f�$h'��?��I�Oze�n��$�������<�����S��b����P$�#q<�v����a:w^r�����:Y@�a:9`��P�D�@1-s�� .q+��v������
L6aUL�bk&��������;O��o�dr[f�H����@3���[�X�z���f�������F���u���Xc�9�FK��jU��:���VE�u�V/������M<�B�H�P�z����}7�3
J�`L2��Q���9p0�26`�@���X�������?���U�P��
������B���P6X�@�\��X�����@��A���l"��+��A
]�VX�
S5�|@�
T�*���/��.�x�B�=w�jC�
�({\j��S��@������5{�T��?��
])�s+{�Z�`�d8���8������R�j%�!\�>6d�������Y�z��Ql��wU����Y(��� ��������=��Pq�w��`�/�S�<��
�C���)f��[���e���r~k�f�d�}"�X��������P��Y���	����`�R��Q{��\j���$��#+�>7V�R~1!���<�-�iX�5�I�o����Z�%�F�d��
�B�
L����@�O������K�H�Km���0����)t����|wvrJ	?Y*W9�Y/I�����tvs3���l�M����S��$Whb$���YQ����?��e��;���x�6	�5K�T�,!c2�e&cd��d6E��3�]�b��M��t��yF�����d8�~`��fw�k>��3Q�����3�#�|���1aq�� On����)e�6�����x:�������x8L��[�d�4I��&��$I�g����;Mz���v��o'���Q�����q/���D`E����jV�I7���P�=�%��f��!����R���j���(��Wg�G�������� ���+�a�~���.<�/��
���'���|�2��_�������Z�	\�\�O��~��Eq�pa��Cu��:�7��;�w���Wzl2��&������'�&���B�p|!� ��
�h���L���l�������v�O�u��T�$���B�vt9$}����%���\w��2
u��|r�WV�:���">�V��?�>9=?>�`'���<s~�������$���)�9��Ys��@�Q:�v�����
4_�[�Y�Z���}�-�������K<B��Ac�t<�<�6K����J��l��Yu����t5�&Io��m�����{�J�������k���'�A�r`���d:n_%�K|�-�#����/��->����je7�����E�M�8s��������������Vc����6�# �lV�Q��V���������K�2�Y��b��T�w�*U�T��7����[k2~y�n���T+��8P08��Y:�o���6-�8~�	@5��3���[[z-!j��mnT��
re�J����ei��j{�Ze�����J>��I�h���t��?��8
H�fQf�o~��as�t����F�
�j*#����x{)��������wg����wg��#������`���;��j2��	��1g���o�n��
v�d��f�(����$��2������I�rp���+�~������f�[��E��=M%��"�G
1a�Th�n0��������o�)���f���)��A���<d;\4*=��������g��w�y��??9���K��f�m��f��On�$�6��GWa�9>;{wv��&x��.:����/":�������?�~}|qt���S<��%�����Os�)�K��Ne���c���u�����]DR?�I�f��|Q���8���CA�*r�st ���0�(q8]������1u�|���?��M-<�'�k��;R/�����y6�4����wLe={G����Z���ie��|&�[���������o���~�����s37t����{��t}�����#�A��m}?{����w26@v��YQ��/�4#�q�Y���7I�N��D��t����6���/���:���������O��D�O��P��7��8�-�o����+8)rA��S��\v�9�����?>�aS.M���bl�_�_���N���U���M��)�BV�����=�MH�&�
�:e��B@O�@����Tpf������@������
����lv�F��jJK9z�
�e������4<B�)��������Q'������:�������D/��9Otu�{
�4r�]�.X�Q�V�t�*�*6H�D+�Z��jUn�;I�;�=��dp����U!4&$Qx��A��U($K�D)(�U.Yv��RV��M&���Ri�T��J���JF�|�cFQ�����+�9�J�����W���v�:�����zc�)r�!QA	T�yZ���U��T\s7�"![U~FnDv�mZV�1p�*G�ky)J����@������-�qu`{���fu��[��7��Js���kD��Z�Z�����D����2�;;r#�"����#�9�%5��a�8p����?v��B���zF*�����Q�,j������j��w�O������Gg�����#�8�P��`u�n��?��
��4��M�)(���f������!Pr����?9�7��;c��	
�(y'TH����X��Z5/2�����2��F��Q�D���������'gG��������8;;pXqf��*a��X�p��
���&D�xf=�9l������'){���q���4��I�a�"�o���88@����$�;}�N�<W���F8�g�t0���h���	�_�dw�I;M~K���s�:��u|#�V�3$�r���PU��S��o���s+DL�O�:���o�4�15�_^����41
C����Rx�2gK�n�����������`����9��8af�'����T���D#�����v���>j�����c����)x
6{����2�2�a�����l����r�"D��Vt1��L���I��LR�0�y��sH�~=��"]��u�!a��j�����	1I
>�I��������!(���rzUR\V&W�����u� @{vt��P�����TP�n���������@�B.�gcw�"}:��By��Mx�����m����
	P������F3C���1K��)\@�T��l��MA�M����~lP[�F�������+�@@v@�G��o�gR)�I��g������]j����%6����
��u���Gh�����r��kx��f6r�$���Z�8�|���2���r-��=;�	����p���z���8p�� ����B��
���hp`.�
���q�y}O�L�����9�����VOkg���x�G��*��?m�����������s���y<�&�&��c<���v�	h�G��|
�0���`���P}Cn����~����mn�=Q^"��dp����*�Bcb�|j�x
���^
���s�@2�@�2(A��(���l��l��Z�*rG[H�C)l;��L/��L��k��g�?�����H����\��q���J�
mBZL��
��f:}��t1��f���B'������V����U+������ �u�k4���l���+�JZf�l:�wg��6)��C4��e<�L�c@�QX �������-^o8d|���O��S{1Yw6�8"�_�	�f��z-������yv|���u�"����;���~�^<�%<�m�"`�Mle�������(a���'z��PnQcI.)��=`�p���[
w���V��J��
������7N������SXew���������X�����
����p�d��h�������F�&����K ��c)�$W�G�����-��)7
@�O&��tA>���-�����M�u������u���_������*�LF	��Sn�p�Z�
h�}.}v�n=h;��mO\��\��>�����'m���������G��#�Z�CPg@�OW	�g8�Q� ��58N>I5�r��\z����LB��s��j[n�Xx�\��?>�;����������s|Gh�YBQW���{��axg�%��� �<�S���Ku��NB�&��a�n��UP�����s�{�O�8���4���w��#5���������`z;7C-�T$���.�x��tvw[�F5�[�Z���
r^�Z�9mjF��2>��bz�����2�=$��#�os����~���J��/����npX0���	��E�5��@F��������C��g�����/�c���M������N�)N��k�;{��+����s&��e��4�'�+<���2���!���#������]����eiXwR.S��N��f�I��[���l%�|}C~`:Fc��&4g����v�%Wo�{9x�j$�Eu���ou����n����q�W���{0�����u >��?����_��Zc�]�r�~������o���@5��@N�!���#�(��r9�n@�Pi���4�a�BI|���&������Sx�\bP�`��������o��o�CL����S���.��h
L�&��q�=8�9�H�Mc>��`�<�&���&���d<>�4�4��X6�����^�������b�"=/.^"�&��C�W�:��~cw���������Y ��<�#�e�x]<�0�G���4y$~�yQzw���k�����L�;���yBM��;q�<l�643��O���V����N����VX���If�x���)��)~�L��\��\������\�?��0���f����wY�_3K�!M�!M��'M����������������������������,i����`��C��C��C��C��C��C��C��C��C���<Y���	�	��=G������L����W�p�US��N���S�W��5�w��z��'�����{�i]SV�ZsP��1z������\M�e&`�!w���;���=����E������G���N���
0004-BRIN-multi-range-minmax-indexes.patch.gzapplication/gzip; name=0004-BRIN-multi-range-minmax-indexes.patch.gzDownload
#26Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Tomas Vondra (#25)
Re: WIP: BRIN multi-range indexes

On Sun, Jun 24, 2018 at 2:01 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Attached is rebased version of this BRIN patch series, fixing mostly the
breakage due to 372728b0 (aka initial-catalog-data format changes). As
2018-07 CF is meant for almost-ready patches, this is more a 2018-09
material. But perhaps someone would like to take a look - and I'd have
to fix it anyway ...

Hi Tomas,

FYI Windows doesn't like this:

src/backend/access/brin/brin_bloom.c(146): warning C4013: 'round'
undefined; assuming extern returning int
[C:\projects\postgresql\postgres.vcxproj]

brin_bloom.obj : error LNK2019: unresolved external symbol round
referenced in function bloom_init
[C:\projects\postgresql\postgres.vcxproj]

--
Thomas Munro
http://www.enterprisedb.com

#27Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Thomas Munro (#26)
Re: WIP: BRIN multi-range indexes

On 06/24/2018 11:39 PM, Thomas Munro wrote:

On Sun, Jun 24, 2018 at 2:01 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Attached is rebased version of this BRIN patch series, fixing mostly the
breakage due to 372728b0 (aka initial-catalog-data format changes). As
2018-07 CF is meant for almost-ready patches, this is more a 2018-09
material. But perhaps someone would like to take a look - and I'd have
to fix it anyway ...

Hi Tomas,

FYI Windows doesn't like this:

src/backend/access/brin/brin_bloom.c(146): warning C4013: 'round'
undefined; assuming extern returning int
[C:\projects\postgresql\postgres.vcxproj]

brin_bloom.obj : error LNK2019: unresolved external symbol round
referenced in function bloom_init
[C:\projects\postgresql\postgres.vcxproj]

Thanks, I've noticed the failure before, but was not sure what's the
exact cause. It seems there's still no 'round' on Windows, so I'll
probably fix that by using rint() instead, or something like that.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#28Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#27)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

On 06/25/2018 12:31 AM, Tomas Vondra wrote:

On 06/24/2018 11:39 PM, Thomas Munro wrote:

On Sun, Jun 24, 2018 at 2:01 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Attached is rebased version of this BRIN patch series, fixing mostly the
breakage due to 372728b0 (aka initial-catalog-data format changes). As
2018-07 CF is meant for almost-ready patches, this is more a 2018-09
material. But perhaps someone would like to take a look - and I'd have
to fix it anyway ...

Hi Tomas,

FYI Windows doesn't like this:

src/backend/access/brin/brin_bloom.c(146): warning C4013: 'round'
undefined; assuming extern returning int
[C:\projects\postgresql\postgres.vcxproj]

brin_bloom.obj : error LNK2019: unresolved external symbol round
referenced in function bloom_init
[C:\projects\postgresql\postgres.vcxproj]

Thanks, I've noticed the failure before, but was not sure what's the
exact cause. It seems there's still no 'round' on Windows, so I'll
probably fix that by using rint() instead, or something like that.

OK, here is a version tweaked to use floor()/ceil() instead of round().
Let's see if the Windows machine likes that more.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-a-20180625.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-a-20180625.patch.gzDownload
0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20180625.patch.gzapplication/gzip; name=0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20180625.patch.gzDownload
0003-BRIN-bloom-indexes-20180625.patch.gzapplication/gzip; name=0003-BRIN-bloom-indexes-20180625.patch.gzDownload
��30[0003-BRIN-bloom-indexes-20180625.patch�\�v�8��-=��3+�%�f;q<m'N�g;c;��������5o��ee:��U��������i["Q�B}U�H��(���z���=�{8���^85G�Q�t0L�v��>8�w��nD����>������7��9�
<��qv���z��{����V���_�D<g7<i�^��#���y��wG���w�c�&������������~b���7vv}q�&n";�-D\���,-��1s���������[8�\N7�S�����_�$���.[,�;q�,��%�
� Jb&`����#k.���	P��DS�lD������`����0fs���Y���=�_0;`���`��"r`p��Q��#"�;b.�7n��R�A��� �d;�!Jck!�$�]��)H�f������^!%����WL�����H���!O�9�.���'X$x�|�
f�)O�D� ����c�� D������lPe��s|P�����m'���M���8N8��L�c�:&��a��+T����X�?uf�
���Y ��� �
���F�(��R�f�Z�,��w�OQ� �c���q"����ziiC��Y�!�w@2���:*�%���5\�D�$^�
���|�`��96�g)
7����di�B�{-��x`6�So���2���=wS�C�&S�D�ER/�;��\�����0 ����	��n�(����90��G�c�0
�b/����v�[�N#�dz�>t�o&�iq7.����|�Br.M��'�"�#�jP{_�I��>���1���������>�-Q��ols0�8��@�{EK'��`�A����]�$���X��E�����2B4�|4N(���8����}�il�n��U�R6,�z����/[�8��g����=������z������HL;� ��S
L�`�o2$��n�	��pTS��w` �(��X���7���VI<����I9��vH�}������0jW���8�0�(
�7�EQc��S����$�M@� �U�,S�'p7�u �q/
L���9:b��T���)�`���z6���@��n�@3���.s"4������
%E"�P�E�?��d/�"���?�`8Z��K�0���Rq93�J$� �w��n��M�-�DC�k���Y��[|���`udu4J���Ys6�H��� �L����2�X�
�������l��nw4|�gm$�l��x}����L�����D�;b�d��:�,6��DwdN,�0z�iwj��9RR`����j����X��-�9�c�\y{��s3$k���)����#���7��{4��3v��=9��C��q]�q��hyRW��`�?��}�Y6:���_�b���%�d|���3ne�d���3��}�n������9k��D�����e�����f_+p��D��GH� R�%��[C�DO���wk��^G���#R=�`�
!�F�B�4�c�����Bo��F�+03�Y���}b{H��4p}X�W �R��"e�BcK���ip"i���J�
��#B�`�����3;Hq�$���� ��`�d��G��@pp@�b���"�?�y�����*q+P:�"J}s7QJ�����S�$��@UJ\��!����]�i?������=`R�o�J�
����?���������CE�4�i���l����������������{R���;6�]��\���v1c���u^�4O�I�-RU�V�$W_��.�~�����J�=VJ�����1��v1�gc7��LR=
^J�m���-��v1�{[���ipBY��B�
�	��]�.hw`�5W$�*W�nT���H
��6��D
 �mq�=��O�t��5R�D�@�������A����~�<
R$��HU�[�R��V��.�������J�[L�&^a���2�|$[���C�@w@k���.�����O������>x��vH+���.f�8�ou������&/��nd�b���6���F���#h�7����W�n��C�������*q�Q:���X]D		l1eq�V��@�W�u�O�:
�/Z�,?���[DN"�?�w�>y�����	'��M������7��3�.cA���Ss���w����A;��7	�W���k/������?�W��|�;��a��t@u��Y<>XR?D�
������d�mH$�};(o��l'x�`�sb �������6?�n�	%�u�=7��8����h]��g��9��@�'N�����3��F��2���������PX�%�%�Y�!�#���I�pu.�]���'\U
��i��l�F�Y����I`/7w������T�����oQ�9��N�W\��j<��hw���y�#��K n�c:1�g���.��DK����2
�K&:��g�cn��L�Bx8&� ��
��`T�an��3��I���-7�pb(v���K>��
|&�>�C1C��,��,Td�JG���6�� ���^�F0��3z�Fk'��2����o�+�E ���G�|�>�d�F&C�S��������6&NR��q��!2�t�`��39�&}��s[�,�d2�����i����Q�%9bl�D�Fx:H��mZ��j���z���<��/�����`�������F��(��S��0����q���!�_��`5��S|�]�u)�V�0�[�����X$8��s��������J��h�N
���^�7�C�b:G�����I�jf��b~4��]V��?��������9��}�����s���|}�.���|��������/p�W���
���e�/nno��q$B�[�fsR�K������`����{�2��K�^�H-�������><���,&��ZQ�]���p�^<+}rw���G��m�C�s|��Yw��:���a���s0i��`�8��o���db�%k�\��Y�����=s~���x�<�VY��\�H�b��$}��n�cY��~���h���c*�$��rx���\\�p `�gl9[�t���~A�����j����B�l1w�S/�QlF���A������f�����AxX#��$�����^�����4tWL@i��'�'�;�j��\+�R��������v�0s��hQ&�'���>LaY��4�$Zp��[��[N���6�qD��W�"�y�X���s5���[q�����6��v���u��\���}�4��0���v!i�p��k���
�,�:��x���1�1�Dz��-Mb���<�DM`C.�G�����F�Q�@�ny{����mF���i���}�))����d�����L��d��(�jR7��1s�	�v���:��
���v���L��k$�=�o��7����������2��m�TH_q��|#��,[����GCy�0�l�Z������}�=������
�-����:0z]�0���h��Z���������
[l��%�-u����'+�#�-6	
���f�� ���jNU�p
<8�3H5�@qM�?�T��o` ���������k����W�o����?_\�� ���%��!7��(D�^D�t��s��Fv<M�_�������&n%��������%�Ue\+L!������c&�s�|���"�FX?2�_���x-\E�(Z�6������w�SI���#��e��5c
��������aLM�:���uH����}G&O�zH��I���p���?�P��<�X������Z�Bo`��j�!�������mlM,���7{���*r4�&3��F�^�<hA���?R�����O��q��� #frWUN�|@;���\y�`��3�.^�b��������+������94�����c�^(�����A�
����\�%K4l�S������]*��z|����p7�n�D�������|�JB���	�����|�[y��x\y�������.�]����%���cX��j(m��M����s\ ��'�Y\3��r�SU�L4������Nn�k{H�0b$��W�_i;In%%�p�/��M���1���������w�w�����^]~xwS�
j�g���\�*��+�P���No~��4�>�S���n�I��o�N�=�0F\[Z��HH�W!�=����/I?��N����L�]�_�����]���Q���������(��J��h��7W����k�d�}9��!����W�?��v�)R/uQ������z"�	����~o����W?��~�=���3��@����
]�
�J���"_b���*��u����o��m�=��Kj���l��[�����Zo'���#�B�.g�6!����4�&��;�JI��gY�(+��%���@\-���*����,�R�K?l�m�0�"X��;���\Z������5qH�ey�LI�"L��a����q^��S4�|�)��Y�8��>�-�����N�H���<��)�f����5�?��b�=�#�n�,�������S����-��\F�(t�B�r�lCA�h�Z^���q5
��Yu
����im#�|�Y���mfaY-bFlc�S<pB���^�T���pf`x�r��[su�^HOWW�Y�g`�AL�V?�|�_.�rg��+F��rj��E�M �{B���na.1�
�""�����\y��7#���sX5,!�,�M(��,tR2$R(�a��s�c�K����1��!�#�w����.p
Q�VQ���������_NPY:��;�N���o��]+m�Jn	�����*�S
tN���
]+v����j��V\�<��R�b�Y5<�0J��/T��b~����������k1{�"��b<�	N^������X�s�����p��,��Z�r	:�'}ko� �Y��]�!��'��G[�H�B+����#/�G��Ih���t34Q��>�h�.*�tAe��p������<'�8�eR�P!M:(t>Kd�$N��MY�P��������R��7V�A���oX�v0�F.d���4h$P��<����I�j���{w$�8�#��T�N���	DNRJ����_���>�F>Yj� =a�j�H}^ m���. ,���4L2�5h�ns�z���`���M�Xb��#\�;���	�Q��<}u�[�/CC�p���
���"�2'XP�\aP��(����6�V
];o9�K���l�����/����O��O�;wv~����hS/
��|����WXxK�u��b����fQmN5y��^������u��l��2"Q��l��u�
iCK=�����w��� <Vl�g���
��&P�\`�I.Q����BM��70X@��Z�/����i�l�r�$a�����p�px�A4���������We�t[
W�{����i��
EdOX��"���#�a���y�O�M9:��d�p�F��g�3��H�M�(�0�O�0X4zF�E�z������0H�h�'�O9��
��F��;@���w�PS�/���3N!�0�)K���o�mf�%�V|��<�=wd�[X8���`���d�������cm6���]����o�d�*�+���e�O����>9����N���~�7����&�(���I��Zp��1���_4�s��
���?4�����[�?hs4PCPr��K��pP��,5m������/�_Z��9��4�]�&��K�I�7����^�\��yCO�y�����K?��yL)��rE���}wG��������-!�@��W����u�<INvo^g�Ab�@a��v�������5��C#@�d�]��������������2������jM*om
���v���M�K���8���q��?;B��'};�aV�����7;�����J���l�#[�������|���lc\�jT��;%��+���"������I;�&��KgYB�_C��L������1�Q=���7�I��
h��-*6I���n�V� ���.;O8z����6������9�`�V7f���t'��a���������Dw6�����h�whadTK0$>j��$��������|��1G�R�^�����<�������+q&�=�px�Jz<�H���'B��H�T��w�B����V����>{��e�wUS�"�+�0ht\'\��E`W�y9����.mY3�H��e�<tD�R��;p���)wS2DM�A���������&�X*b�d���Ui���>��&�U+\]��o�|�#t�(bh�~��4���W�V��.y�w��+�v]	��G�S�uD	���p�Q�%"�!Vhp���Y�wk�oq��{
�46���5-�RNM��T���{s[�z�|�\I!��1���|�JXw����_8�Tf? �'�a�`!�9]'�KAE��XS��G�-Em6E%����K��|�
�/no��Ea
 	��������Q���@���9d��N�WdF���8�V���`{�A���6�'a,��enj&�P"n�n@���m�M��d �Z{:n�s�����0�QO����(�z�]t���r��-B����As/#��0��e�����{�������J�}6J`d!�,���9)�R*���	�G�BE��`cu���5�?�>�i�'.|>������%�W�>��
��"��?�M6�o;$rr\r��C
�+_���l���GS
�A��d�7�+�	?��� �����)�y�|�����[������[���H��X<�4�2�C������1��t������Hp���	�&���`�I�1�(KU�kk�}�9��`.�N���z�X!0�eg~J�}T����_8��:	J����#���1���A��,I�9<��gF��h:���� ����$�LY2����a���
������>�fnF6�0��G�3�&��w��F��Q����������Hx��0L@	\""~�j��)�;��L�#��@C��t:�V5Z
�j3�'�'�2�1�O�����U|s��R���w��x�F�������g|q�8h�&�)(��'Q��!iP�h���oP>�7���?�1����/5o����W�J}��>�o������2_?����A�����!���4�h�2OK��,��S������J������XN���&���-A2�V�KO7���W�>!4�
:��c~�;�'�]��)�r��<Z0l:��:��p�N+�9�Z��1��dz2���K0�t� ���:Q�'����3n�������>l�!U:z%�������
HV���=���|o��O�����Id%|3������`)����iA�
��������T�\��/`��-Cj;���0��y���Y�0lE��	��>�"gI�_��C���XW�}�<���b;��e�^����zH/�����[+H�Z-�01�<�-�X�* d�Yf��-�1���o���G�r���>���	y�3'He�����1���r�������6/4w� �A�i���=��g��pC[��h��v�\m���d:�]�����u�`��($.G��L�)1�$���2,)�\�$5�W.�2;}������9���l �Xt��t����QtE7�7]>m�K3������A�Ks5����:�F?��j�F0��[�|�|=�"�Y��$i:>�g�7q����N!������������ �9���P���<h�
���J�3��%���J�����$U,��&g\�G�r���`e,O��~r��1z�B r����
�(s0�Y����P�Il� ����STz����d6�����BF!��{A��A����HXy����]y���D�0N����K([U�r/vv�DQ|���"-O|D���� �I�3�R�7��-��2
���<���=JxF�o��@g��8�>�$�3���[<��GG,S����8����*m`D������A��Q�=g�h���0qO�/&>��n�\�-�_��T=�^'i�������{�
��uJ��,��i<�����E��������?}2������&hFb�N�t6�f��1���2�M���a�����9��*��0�3��ldv��J]P���������3�����
zD3�B�_
jF�vl�!dXr%�!���q%�q=�l1_�RN���'����q�2PS�nquW"_�ju�p��y9�G"HJ�w�J-����VH0n3'H���j��_*�b���crO/��N^�)0�w�Lqv|������������
��f��1dV}C�
�!��2oP��F��A��:v���OI���Y��of�(�VHq
f�o�LU=�2�4�t`�R�HS95j����L�����A`���X�
�L�?�%�o.���G~�����A��r�q9��xD�;��
ad-�v�72RV+Q,*�q�f�������vq���$�e �����F�TU��#x�x�r�A�=a5weo���a��<���Zl��>!�tt��v�}�}}�����l>f���C1��O����� �Dt���w��"QQ�rf"^���d��2�.�����C��|���hO��t�~�N�iqVv�d0�q$p]���tZ����B���X �Z��d����QS����v�� |��h;����pXt?P�]���7C�C���U���%|.��zT���&��t��-vu�z��I�l'�l6����� <kMC:�=1�I��uZt�t9W�$5��AE�t�U��J9��_��J
�-�������{�[LT�vtx&2�&�1�3avZ�(�U�*ux�I3�
X������)�1_wP�i4��3��*kd�hJ�|)P:a"U���pT��g"%��dv
1�xR&�:�q�����ZQ�k�R�3Lg8�dFi�X�XM#��=�?���O�/N�������}vt��q>��+�$��k��D���h�lnOr�!@"������l���`d�@3S��CA��0�6HX������"FK�M>����Mu��s���K�G��"��D�P�$�$A@<x���1�B�����*)E������CD	x��������9h���������:����������S��p���b��%���bb��b��6*����!x�}�"��]�������[)�P����'�����@�3As�ul1�����C)�'m~�z������/z#6�
p�r�x7������Jx�,�A����F5l��
��s�`��V�W�����%l�l8>K0p��G�\A��S����
M�����]����n�u����b`Fw<K ��Oh�	�X���n�<gc�"*�@:�0�3������_"J����@��pSh���P��t�8:��M��p�8E\A6�D�/�'��:* [8�����T��dv�k kE�&rV��+���B���4�fQiB?*�!^s���7���H�J����GS3r��.N�ca.�q9e�?����aB���{��qJ�>�xwt~!������*�[�A�F<%%�{[X0�����	Dj�+�y��8AA@Z��@�p��������U
�_zk���������q�A%z$5�<�D�T[s����<�P+������.�.Lc@s{�w�8�+gkUc�?�	�
-P�[�`�~Hn3�7Vq�E����u�^]��x0hb-K���}�c:���<���F���-������[<0���c!q����DQK���x���l���L���^(~_C�0���F����G�[���_�+6��?u&@O5��t�A__0D��i�kH"����l��>K�C1��7zC�i��A�$��Z����d�	�p0��L?����2eO���O���M<K����"��8�wDaa�Q3�GH�����������2��N�����.�����c�}���� ���x3������7��cJq4$S���d��^�1��Q���$�~/U��������+ll,�1���}A��QL�b�����x{{�Pt&I����I-����b"����<>;{wVf���g�/�	�����,��A,��2�|��b��v�c�&_r��o���?�	����l�D��]�Q�b����5����I���Ev�u��pH����k	������y�J�|��P��(%����]�th�.�q��7�]q�ZB�����DDpC#n�v�$����1�0����H5��Ec{k���[�k'�+�p�}��nt2�������%}~�C���4hk]�=�2w�>5��XN�����L��p��u>��w�7L�S��gr��a������.��Vb���c�q����d�G����*�W�:�H��;
�=")�n��tR�!�3���8����G����p$���8�e<����g��)U�i�����p�^a�n �r�N��>�-7�y����1��mS��RyP� ��/��2��u��Q&�K,���OI���1:K�J�`��H$�8�k�u�����������,� �� �C�L�Id��dri������	��t#m���� ���Fk�c��Q�9E��a�e"�w`��I6�X�a3�d%"1\��ff\C�j�9*��(N�@Y&T�����b��s������Y
n��K.����	"�#e����m;$��.�%��q������o��p?�(Z�6�n(��<�D��e�����-S�l3B\�]?#tW���5��l@�9P��NG�z`���$��#*�r�,�$K�xJ�I��
0�?�PEzo^�U��������`�|7���3f��Tq��
:�G�����!I���0%D_���<����cj%�{��GE	����p�
j���6P��P�>�afM��f��	`u���H
G�8:��c,@2�{��NQe[FV�P��/.��ga��/������F8���6t
�UI<�Qzx�3@[����[g����P�H�����
sD�	��j�B�U�\UT�;S����+���z�q`�����RU��g�����{���r6�N�'�%�Y����^z:\5$c,���m0���u[�O�%m�
�O'�o����`Vw�����2����g����P���z�@����k���n�W��&�j�}��k@o�tWcz�y^7Q^3 �x;(^�}Q��^��R���6�
����V�8}Y�,��SZ�����3rYu!���c�5]By�E�'������#D��g�>&o��F,�L^NSfuQ�8%?g-���Q���������>���e�Ja������m����*�|y��<�\�)�.��yO�q��j��Z������Eq��:��l�Cs��>0k��F�����Rs&�U����dEA^Lrg�&a�mXo������;������6�@�0����2�����_
_�[k���������r%4�>L���h���o����h�x��������FM�}A\������������\��������O����1..�5���;�F�����d,��;/'��|����.+��������������zsx�K�Z���G�����%���l=�10�14�������)����f-���l����=�
1��@��a���Z�5����#����b���
�#��L�;;�	�G�j��d4s�S���!�����-��6^����F#���j�Z���u{���F�_����z�fbV���
���
|kd�/��}u:�r�H�tz#��l`����������X
�bS�i�H�k�8&���g1s�^��}��e6P�%�����M){?�M"�'���q@�g���������i�f�	�j��H{�WPX�X���$����rg^�P�N�����wZ����~�S�w�U?C�F����
_����W��g�Lo��M��?�~���l��l���:@�
q�w��/K����6�r8�W�����@��N�6��Ol���!�X�}�7U�T*�'(y�m/|5�a�m?������>������|�)^%��h\e��{����Ie��u[�IU�+Qu}����yDir<�E��V��I�P�f$��K4Q���BQ�^Q�e�(��W�D��^DT�#l�fFTD�����B�
�8I���,��$���2��Z�^4�CZ��)���#	��o��u/9�K9x�z���CNk�G9�B��Y9���&�T��e��F��}MX���Z_7��J��^�`
���^���p����+����a
�����k���Z�W�����o��{s�;h��d�^�&-�xt���}RO[�B���{��<"l�D�x���)$�������9��HR(���(�5������x+�����ib�H-m5
m
��6-���(�%��)�#!�I�@���.K?
�si8��pJC��<5h��U�:fs����Pm����#����/o�v1I��W���!���4�;�f��8p�0!��^
�W�]��{�q1�����c���T��:��9�_��	u�9:��Dx�U+�"y}w�^���}�z���sKt���Eka��b�*H��-�HU���]`"&yE�������f>
��E$�7���j�}�K���Ms1*7���Dg�"�������&%^����4�|��HJ��5����e�l)��ED�F����G��R�#B�R�W����c/%^�:9�Y���h�B[�z����f���������(<��	w�S�2}�SZ��i���4�����"/����,�U��D��~�c<�n�;t��\�7[
�uR�?����R>�����G8lf��6�Y>+z0�<@��mE��0;�@�Cr!N�����N�����[��9@7�`������f���D��l�)����C1�U�F��\"�Kw� p���o��z'��3.}�j���n��|1�-9	B��hG�^?���]������ ���,�ke�xsI{s�*��+���`47��I�E���k�����xXt����c3�Z/���>���h�y�����e�%p�W�)8�B��;�����q����B�qj�a����2�WI���C����+��EJ}�^h���V�3ZP���9����b)��C{>�~g�5���9t�-|D�[!�
�|���:��4*�N���:���K_�/����/Z{�4t{N4�������s�/0�����0
���44������Z�����^��],�}���u�akQ��0����3N��b�0v�����y����d�+.�bK����_�o�#r�7��g&7��a:
�'�-�G8M����t���H7e�S�ikM!����g�t�OU��+��V���z�����a!���;/�_�a���^�[�o�����V��h&���~5���~��u��Y������;�>'��3��
w}A�2_����8� Q�����4��^@��jv��f��B|��EE���_�Zt�*�����FQ��V���j��������J3��R���J:r��U�I�uA��1sN�p�!�y"���"kldM��D|�	��d��x�!8��?{��+���`��S�:N���k���'3���2C�4y��CM3�it-'1��*$��@[��XlKI�u��Cb`COr��4�����_��t9l��Y�*��%I�5�����V�$Y�z!0�����J/��)$>Lz�������`[��Yt+5�o)i�F~slh�-.QT��|�s,
����E����(x���7�
�V������
X��]�Bw�+`Q��i,�o�P/HMu�JN��3�Sm�T�S�����5���.H�,u���9�4sV@�r�cM��Bt�	����5����X�9$6�f>��s�>[���6l2�7���5j2?
*��j4�Q�P���E�����A8��F���Q��YH��!��k%d�-%=��T�����N���E�q���hU�F�V/�Z*��~��V������E��#�tj%���`h��(�	�*D�:�[�hX'��"��ZY��=��=���Qf�k������5�8|��&����X�`M4�}4S������m���
�Y��e��x�\�"Q�`�4�?,�kfjU�f��VaM>W�"�����!\��X7�+��rY��vVt�
���8J�Vy����: N��_#�����[��Mz{��a��j��4��
�u��J9�N8W$J�������Ct���
<�f�XU��)A�{$A�+s������)I���.����{&�����nk�m%�nk�n5!oko�������21N\���`���V)M���J�G�9��QiIu$W��5yg'k��N� � �f���$�f��&�f�.�w�����3���D�� H�����Q���~��s���������p�;k�D{Q�d<�z[I<�z@[M<�z`[.�aM<��>�1 \�^��7_:�4d���e��9��K@�����F��������\�n.���)��(���-��*$����s/�.���{�����iuP��?�I�����
�WYLw��,��B{�����z_���h��*~��t��s���{].w�o)�w�<��-����W��3_��5S����6�=F�ZT���u�6[���s52ED��9BkrU�BkseYCk�s���>���K�T�=���������N�@Wuk���^���^�X�x��L���,@C��4O?Z�&�V�V�t[�+s��
�e��z������9�����Q3���k�(�����oe���Q�I#��42��.)IFf�XVF�:�bF�J��0� r`��$~`�0�&�`�@.+I������l��K���U�����Ml�j�:��(���h��PZ,���r�^(��,Y����0kl�eM��D��	���"����>���� ��]��j_[���_�g�_�EI���*s4�v����Y��5g�kk��J���./�
�*�Z\�xX+����e?����|"CSw.�r�����;lQR=�JB���3����l��H�MoD��n���6vw�I��hU������`�����X���,��X���W:o��g_��PI�.YU����E	��:J�f]i�����������N`'/@&�������{I?�
�x�Or<D�`�gva�,���tB�
�||Y��NXll��;#����A�]Q7�.��PW
�f>����NXld�R�wFA��hhF]�4�.j
�||�Jr9��Q&�1.��hu(�|(�AY�*�E>�EK���:��}(�/����PV������Bj�����Zk9u��Ph����y���F��tugd��Dype����G5sr��9�`�GR����pwd�0Y�}rtah*0U	���8������m�(	j>����l��"+w� �X��{�Y�"����_�ZwF����w��YP3�8r��{�*j5��=�]�7����H�A���9xu��L� �OpWI\6�u!�;�Y"���4��M6�z��1���J��1���P�� m��������d�����
c���H�uB�����9���6��*@��A^��P���e"�s�p��,�x�s}�;���Ys��"t����xw����� ��������s@@Pl���[Y E���0��0�h>��1�Nxl��H��8�"�W��\����tw�#W��\*��U�4��
���H����������2b���<@vlz���c�osf��������������������K�j���vm��������(FLnv\����>�&��S S[���`|�'$`1�j�r��-�V���Z�2�+DEw4�MNG}9?�� P�!i>�!*��p<3x���L�O�$��'�2�G��d�B��iYQ`l��b�)�@��|�F(���8[;NXl��0�;/9g��a�X�,��0���f(t"\ ����9�T�
��JR;h�CTGt�&��*�b�5���Vf��c���'���Y2�-����Q���t���u��D�C=n�Q���Ovw�Q����:�Z`��1��`�%�u�*�F�m�g�"��x+�����}ED�G�&�^!e$G�}��P�����%w0&I��D��8�f�F `�������l���������*w�e���R�lTE�@�u(�V X.S����arX��E���]K6C���� ��D+,���S>��*x�s��B�`<�t���;k�!jB�=.�
e��
Pf��J�@����X*�s����@�����=X-p�I2�?X��q�P�eO�T)X���C��M�qJc�,x=��(6d���u�a�,���g�DCwi�����^�8�;�m������
V�l���������������A]H9?�5P3T��>{�j�Xy���U(_��f{��U�ikO0t)���=�P.�lH{����@��Q�?���@_l��4�����7���F�_��n��2
�H�f�j&�~kk���J��M��%f$��6����B����:
��r}�;;9���,��������	^��X:��O�Y6�&����)C�I�+41�^l���_f����2�����x<X����c*V��1��2�1�{V2��I��D�.^�gW�&A\�v��<#pK��d2w?�Ir3���5�Xj������j����K>�������q��'��Qk����E�Z����a<�~E�n�I<&C�-\�m���g��r������M��&�gzv�P�����(�������H�F"�����m5�����wz(������F����qA�aW)W�v�\o`�������cvq���ct�@���������p�m�f���p�W�~CT>v\���JW�k�]}���.�{.�'��?����K�0I�������g���I�����B����+=6��c|Fw@M�~���Wd�x��|�aP4gUr&S�Y���V��i;���:�T�{u�]�{;:
����U���|�o�;�O����~>��+q�cpV+[�m����]����wy�9?~{������sOK�����U���up��(�q;wb����������,V�G��>�����HLq��%!���1:��S
~�%|T�W��x6a���[�zV���z���]���^�����=c%�`h�����5�����R90�rq2����%>����|LF��x��u�C�f����v����&�9e`U��q��X�l��|�V��Si�T��o6+���rP��]�V�|�t|��n��|���Is1�]�v*�;\���*�n��S���5�<I7s���v�����,����^|������A��g���-���
G�67��s��}%�b|����J��r������F%�
l�$G���I�c��G$T�(��7?�������YUJ#��g|5�}e�L��Wo����?T�����g���1Z�{{���l�\z�tz5��d��3����o7SL;~2�c3T�BYW�I��}dK�?Z���Y98�K���f��O��A��-V��i���|z������0�Z*�\7�]Fz��f���[�z\��v��]��}�^�.��pr������A��@���<b����~O��%�U3j��N�3j�'�o�I��A��������;;`D��`�Yg�f�D�c�X�m�>�8:y��)r�����I��9���%�x��[E��1����T����."���}3NS����\�Jt���K���:l����s�8���[�@���:��m��`�����������g�rM�<�~�����;��������u�������|>������j}SB���hU?�o�k�����}���=mr��y�S���� ������oj��; ����(���I����,Cr���r��C�AN����F�y���_�b��_|��^�����"�'�|�x���r����7[��������)[[.����OGo�����)�&�Ht��
����/��w�G�����c��&_��	!��~�����&$t��r��2�TO!��L ��lV*8���w��\�[�n@��d�n@6�U��~�����l��2�z��ER!��s�r�K�p���dssS��]H�ul�y����'�:��z����Q����{���L$B�_��F�*7����F����g2���@���(��� q�*��q���*�,;��)+��&��r�Q��v��N��Z�%�e>V�1���y�����Z�@��������Tj;Q�U[��A����������*�<-Vi���Tv*��Z���*?#7"��6-����X������%AtA~��A�@n����:�=��^��W������^���j�5��~�U�U��j��P�`p���E��q���������b��N��;}w�r�Y�#��YB��e5��]��s5��;���������c�H��?�����g�������7���_~�Ki�������B�q3�G���T���(9�w�����o���	W����	}���	*�B��e�tC����_r�a��F#���`�i�������������s|
{�����B����8��3rz�0@��P�w��MvI"p<����6[�Ly�N���=y��8�US�����0@��������c���?|�x���f'o���Lf#�
���Y:]�o4g��
��/X2�;���&���x����[���:�w����9X��	��I��{���W��"&���?����G�����//K�V[���!��_���)�{��%h����O|{���l��w0�K����P�03��MMDs*���f�J_�y�;���d5�����1�������A���Sv�L����X����x��gcm9l���R+��Sb&���$�B&����<����9$J�OH��Y�:���tN���������$��$Ig�i
�I�s��dt9�*).+�+�@M�z�:H�=;:}M�����TP�n��/u{K#x;�(�\
��$�^1D�tLs������F���������,� �'�s�'�f��+�b���S��6h����X#�����_��{��,�<���F-	1/N��W�������4�����R<��}�D?�;{���)�Kl���1���pm�3��>;��9��K���1�l�Ir���q4X�Ld����Z�{vl"Yc�����+�p���+@�!:�+G<��1B���7 ���\l=�����:�����Gi3s������������>��KU6������W����������x�M@M��xf����@�����a,�{��������
>��E��A�!��{��D>4Q)���w�CU����(����&n]-�������d2�eP�beQ����.�� ����T�������R�v���^����a3�E���/p���%���)`5�6������z��S� t0����b0��K��N����k����S�2Vx5O,�g5A.���h"�{�����W�'�����t�1��f�mR${�h��:x"��6�����@0�)�
& �_F[$��p����'�6!7��b��l�->pDj�"f�6��Z �	2+q�����=��`ER�9
v�W�'���x�Kx0�,E�b���V��DSQf�Q�+��O.�f:������2\R�a{���$�{�������
��b5Z�U/V��Yo����)�?):����w=j�[{�Q��6<��!�k�����-���	9{��� MY�1��@&87�R�I����
%�[��Sn��7�L�!��|P�#[>�1�1H�2���+��mY���3���??��Ur���k����
L����\���z�v������#���}B'�	�9O����������G��#�Z�CPg@�OW	�g8�Q� ��58N>I5�r��\z����LB��s��j[n�Xx�\���?>�����������s|Gh�YBQW���{��axg�%��� �<�S���Ku��NB�&��a�n��UP�����s�{�O�8���4���w��#5���������`z;7C-�T$���.�x��tvw[�F5�[�Z���
r^�Z�9mjF��2>��bz�����2�=$��#�/s����~���J��/����npX0���	��E�5��@F��������C��g�����/�c���M�������R5R�����w�L�W(�W1�L�Y��ciO�Wx s+ e��%CN��GNs���{�8��_�����\�Na�$������2��8�J������t��CMh��I���K����r���H���^����6{���(����N��Y�`�a7�7�@|��B���67����J���ns)#�_���j�I1��\C:�_G�Qr��r2������d��i������qM&����	��0������
��#���	��>(�4��RO?�1���]����8M�c���zp�s�����|n	��y M�)�M�7m�x|i�i<���l�mW��N�����-N��Ez^\�DrMd!�h�u�����n��i5�I�_��@�Cy�G��F��x�a,���i�H�����&��"���w��5�!vdq�����N�w��y��mhff����Y����k�z?�����tq���L�J�!S�!S�n�������A�����a�����������f��C��C��N��#Q�!U�!U�!U�!U�!U�!U�!U�!UY��!Uy�T��d��d��d��d��d��d��d��d��d��y�������s������������)� ���������6;�k�������O&'�!�������������%c���;�����4�L��C���#w��#{�k'��?�v����G��?r	+�p�
0004-BRIN-multi-range-minmax-indexes-20180625.patch.gzapplication/gzip; name=0004-BRIN-multi-range-minmax-indexes-20180625.patch.gzDownload
#29Michael Paquier
michael@paquier.xyz
In reply to: Tomas Vondra (#28)
Re: WIP: BRIN multi-range indexes

Hi Tomas,

On Mon, Jun 25, 2018 at 02:14:20AM +0200, Tomas Vondra wrote:

OK, here is a version tweaked to use floor()/ceil() instead of round().
Let's see if the Windows machine likes that more.

The latest patch set does not apply cleanly. Could you rebase it? I
have moved the patch to CF 2018-10 for now, waiting on author.
--
Michael

#30Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#29)
Re: WIP: BRIN multi-range indexes

On Tue, Oct 02, 2018 at 11:49:05AM +0900, Michael Paquier wrote:

The latest patch set does not apply cleanly. Could you rebase it? I
have moved the patch to CF 2018-10 for now, waiting on author.

It's been some time since that request, so I am marking the patch as
returned with feedback.
--
Michael

#31Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Michael Paquier (#30)
Re: WIP: BRIN multi-range indexes

On 2/4/19 6:54 AM, Michael Paquier wrote:

On Tue, Oct 02, 2018 at 11:49:05AM +0900, Michael Paquier wrote:

The latest patch set does not apply cleanly. Could you rebase it? I
have moved the patch to CF 2018-10 for now, waiting on author.

It's been some time since that request, so I am marking the patch as
returned with feedback.

But that's not the most recent version of the patch. On 28/12 I've
submitted an updated / rebased patch.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#32Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Tomas Vondra (#31)
Re: WIP: BRIN multi-range indexes

On 2019-Feb-04, Tomas Vondra wrote:

On 2/4/19 6:54 AM, Michael Paquier wrote:

On Tue, Oct 02, 2018 at 11:49:05AM +0900, Michael Paquier wrote:

The latest patch set does not apply cleanly. Could you rebase it? I
have moved the patch to CF 2018-10 for now, waiting on author.

It's been some time since that request, so I am marking the patch as
returned with feedback.

But that's not the most recent version of the patch. On 28/12 I've
submitted an updated / rebased patch.

Moved to next commitfest instead.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#33Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alvaro Herrera (#11)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

Apparently cputube did not pick the last version of the patches I've
submitted in December (and I don't see the message in the thread in
archive either), so it's listed as broken.

So here we go again, hopefully this time everything will go through ...

regards

On 12/28/18 12:45 AM, Tomas Vondra wrote:

Hi all,

Attached is an updated/rebased version of the patch series. There are no
changes to behavior, but let me briefly summarize the current state:

0001 and 0002
-------------

The first two parts are "just" refactoring the existing code to pass all
scankeys to the opclass at once - this is needed by the new minmax-like
opclass, but per discussion with Alvaro it seems worthwhile even
independently. I tend to agree with that. Similarly for the second part,
which moves all IS NULL checks entirely to bringetbimap().

0003 bloom opclass
------------------

The first new opclasss, based on bloom filters. For each page range
(i.e. 1MB by default) a small bloom filter is built (with hash values of
the original values as inputs), and then used to evaluate equality
queries. A small optimization is that initially the actual (hash) values
are kept until reaching the bloom filter size. This improves behavior in
low-cardinality data sets.

Picking the bloom filter parameters is the tricky part - we don't have a
reliable source of such information (namely number of distinct values
per range), and e.g. the false positive rate actually has to be picked
by the user because it's a compromise between index size and accuracy.
Essentially, false positive rate is the fraction of the table that has
to be scanned for a random value (on average). But it also makes the
index larger, because the per-range bloom filters will be larger.

Another reason why this needs to be defined by the user is that the
space for index tuple is limited by one page (8kB by default), so we
can't allow the bloom filter to be larger (we have to assume it's
non-compressible, because in the optimal fill it's 50% 0s and 1s). But
the BRIN index may be multi-column, and the limit applies to the whole
tuple. And we don't know what the opclasses or parameters of other
columns are.

So the patch simply adds two reloptions

a) n_distinct_per_range - number of distinct values per range
b) false_positive_rate - false positive rate of the filter

There are some simple heuristics to ensure the values are reasonable
(e.g. upper limit for number of distinct values, etc.) and perhaps we
might consider stats from the underlying table (when not empty), but the
patch does not do that.

0004 multi-minmax opclass
-------------------------

The second opclass addresses a common issue for minmax indexes, where
the table is initially nicely correlated with the index, and it works
fine. But then deletes/updates route data into other parts of the table
making the ranges very wide ad rendering the BRIN index inefficient.

One way to deal improve this would be considering the index(es) while
routing the new tuple, i.e. looking not only for page with enough free
space, but for pages in already matching ranges (or close to it).

A partitioning is a possible approach so segregate the data. But it's
certainly much higher overhead, both in terms of maintenance and
planning (particularly with 1:1 of ranges vs. partitions).

So the new multi-minmax opclass takes a different approach, replacing
the one minmax range with multiple ranges (64 boundary values or 32
ranges by default). Initially individual values are stored, and after
reaching the maximum number of values the values are merged into ranges
by distance. This allows handling outliers very efficiently, because
they will not be merged with the "main" range for as long as possible.

Similarly to the bloom opclass, the main challenge here is deciding the
parameter - in this case, it's "number of values per range". Again, it's
a compromise vs. index size and efficiency. The default (64 values) is
fairly reasonable, but ultimately it's up to the user - there is a new
reloption "values_per_range".

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-a-20190223.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-a-20190223.patch.gzDownload
0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20190223.patch.gzapplication/gzip; name=0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20190223.patch.gzDownload
0003-BRIN-bloom-indexes-20190223.patch.gzapplication/gzip; name=0003-BRIN-bloom-indexes-20190223.patch.gzDownload
0004-BRIN-multi-range-minmax-indexes-20190223.patch.gzapplication/gzip; name=0004-BRIN-multi-range-minmax-indexes-20190223.patch.gzDownload
#34Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: Tomas Vondra (#24)
Re: WIP: BRIN multi-range indexes

Hi!

I'm starting to look at this patchset. In the general, I think it's
very cool! We definitely need this.

On Tue, Apr 3, 2018 at 10:51 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

1) index parameters

The main improvement of this version is an introduction of a couple of
BRIN index parameters, next to pages_per_range and autosummarize.

a) n_distinct_per_range - used to size Bloom index
b) false_positive_rate - used to size Bloom index
c) values_per_range - number of values in the minmax-multi summary

Until now those parameters were pretty much hard-coded, this allows easy
customization depending on the data set. There are some basic rules to
to clamp the values (e.g. not to allow ndistinct to be less than 128 or
more than MaxHeapTuplesPerPage * page_per_range), but that's about it.
I'm sure we could devise more elaborate heuristics (e.g. when building
index on an existing table, we could inspect table statistics first),
but the patch does not do that.

One disadvantage is that those parameters are per-index.

For me, the main disadvantage of this solution is that we put
opclass-specific parameters into access method. And this is generally
bad design. So, user can specify such parameter if even not using
corresponding opclass, that may cause a confuse (if even we forbid
that, it needs to be hardcoded). Also, extension opclasses can't do
the same thing. Thus, it appears that extension opclasses are not
first class citizens anymore. Have you take a look at opclass
parameters patch [1]? I think it's proper solution of this problem.
I think we should postpone this parameterization until we push opclass
parameters patch.

1. /messages/by-id/d22c3a18-31c7-1879-fc11-4c1ce2f5e5af@postgrespro.ru

------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#35Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: Tomas Vondra (#22)
Re: WIP: BRIN multi-range indexes

On Sun, Mar 4, 2018 at 3:15 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

I've been thinking about this after looking at 0a459cec96, and I don't
think this patch has the same issues. One reason is that just like the
original minmax opclass, it does not really mess with the data it
stores. It only does min/max on the values, and stores that, so if there
was NaN or Infinity, it will index NaN or Infinity.

FWIW, I think the closest similar functionality is subtype_diff
function of range type. But I don't think we should require range
type here just in order to fetch subtype_diff function out of it. So,
opclass distance function looks OK for me, assuming it's not
AM-defined function, but function used for inter-opclass
compatibility.

------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#36Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alexander Korotkov (#35)
Re: WIP: BRIN multi-range indexes

On 3/2/19 10:05 AM, Alexander Korotkov wrote:

On Sun, Mar 4, 2018 at 3:15 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

I've been thinking about this after looking at 0a459cec96, and I don't
think this patch has the same issues. One reason is that just like the
original minmax opclass, it does not really mess with the data it
stores. It only does min/max on the values, and stores that, so if there
was NaN or Infinity, it will index NaN or Infinity.

FWIW, I think the closest similar functionality is subtype_diff
function of range type. But I don't think we should require range
type here just in order to fetch subtype_diff function out of it. So,
opclass distance function looks OK for me,

OK, agreed.

assuming it's not AM-defined function, but function used for
inter-opclass compatibility.

I'm not sure I understand what you mean by this. Can you elaborate? Does
the current implementation (i.e. distance function being implemented as
an opclass support procedure) work for you or not?

Thanks for looking at the patch!

cheers

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#37Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alexander Korotkov (#34)
Re: WIP: BRIN multi-range indexes

On 3/2/19 10:00 AM, Alexander Korotkov wrote:

Hi!

I'm starting to look at this patchset. In the general, I think it's
very cool! We definitely need this.

On Tue, Apr 3, 2018 at 10:51 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

1) index parameters

The main improvement of this version is an introduction of a couple of
BRIN index parameters, next to pages_per_range and autosummarize.

a) n_distinct_per_range - used to size Bloom index
b) false_positive_rate - used to size Bloom index
c) values_per_range - number of values in the minmax-multi summary

Until now those parameters were pretty much hard-coded, this allows easy
customization depending on the data set. There are some basic rules to
to clamp the values (e.g. not to allow ndistinct to be less than 128 or
more than MaxHeapTuplesPerPage * page_per_range), but that's about it.
I'm sure we could devise more elaborate heuristics (e.g. when building
index on an existing table, we could inspect table statistics first),
but the patch does not do that.

One disadvantage is that those parameters are per-index.

For me, the main disadvantage of this solution is that we put
opclass-specific parameters into access method. And this is generally
bad design. So, user can specify such parameter if even not using
corresponding opclass, that may cause a confuse (if even we forbid
that, it needs to be hardcoded). Also, extension opclasses can't do
the same thing. Thus, it appears that extension opclasses are not
first class citizens anymore. Have you take a look at opclass
parameters patch [1]? I think it's proper solution of this problem.
I think we should postpone this parameterization until we push opclass
parameters patch.

1. /messages/by-id/d22c3a18-31c7-1879-fc11-4c1ce2f5e5af@postgrespro.ru

I've looked at that patch only very briefly so far, but I agree it's
likely a better solution than what my patch does at the moment (which I
agree is a misuse of the AM-level options). I'll take a closer look.

I agree it makes sense to re-use that infrastructure for this patch, but
I'm hesitant to rebase it on top of that patch right away. Because it
would mean this thread dependent on it, which would confuse cputube,
make it bitrot faster etc.

So I suggest we ignore this aspect of the patch for now, and let's talk
about the other bits first.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#38Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: Tomas Vondra (#37)
Re: WIP: BRIN multi-range indexes

On Sun, Mar 3, 2019 at 12:25 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

I've looked at that patch only very briefly so far, but I agree it's
likely a better solution than what my patch does at the moment (which I
agree is a misuse of the AM-level options). I'll take a closer look.

I agree it makes sense to re-use that infrastructure for this patch, but
I'm hesitant to rebase it on top of that patch right away. Because it
would mean this thread dependent on it, which would confuse cputube,
make it bitrot faster etc.

So I suggest we ignore this aspect of the patch for now, and let's talk
about the other bits first.

Works for me. We don't need to make the whole work made by this patch
to be dependent on opclass parameters. It's OK to ignore this aspect
for now and come back when opclass parameters get committed.

------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#39Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: Tomas Vondra (#36)
Re: WIP: BRIN multi-range indexes

On Sun, Mar 3, 2019 at 12:12 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On 3/2/19 10:05 AM, Alexander Korotkov wrote:

assuming it's not AM-defined function, but function used for
inter-opclass compatibility.

I'm not sure I understand what you mean by this. Can you elaborate? Does
the current implementation (i.e. distance function being implemented as
an opclass support procedure) work for you or not?

I mean that unlike other index access methods BRIN allow opclasses to
define custom support procedures. These support procedures are not
directly called from AM, but might be called from other opclass
support procedures. That allows to re-use the same high-level support
procedures in multiple opclasses.

So, distance support procedure is not directly called from AM. We
don't have to change the interface between AM and opclass for that.
This is why I'm OK with that.

------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#40Nikita Glukhov
n.gluhov@postgrespro.ru
In reply to: Alexander Korotkov (#39)
5 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi!

I have looked at this patch set too, but so far only at first two
infrastructure patches.

First of all, I agree that opclass parameters patch is needed here.

0001. Pass all keys to BRIN consistent function at once.

I think that changing the signature of consistent function is bad, because then
the authors of existing BRIN opclasses will need to maintain two variants of
the function for different version of PosgreSQL. Moreover, we can easily
distinguish two variants by the number of parameters. So I returned back a
call to old 3-argument variant of consistent() in bringetbitmap(). Also I
fixed brinvalidate() adding support for new 4-argument variant, and fixed
catalog entries for brin_minmax_consistent() and brin_inclusion_consistent()
which remained 3-argument. And also I removed unneeded indentation shift in
these two functions, which makes it difficult to compare changes, by extracting
subroutines minmax_consistent_key() and inclusion_consistent_key().

0002. Move IS NOT NULL checks to bringetbitmap()

I believe that removing duplicate code is always good. But in this case it
seems a bit inconsistent to refactor only bringetbitmap(). I think we can't
guarantee that existing opclasses work with null flags in add_value() and
union() in the expected way.

So I refactored the work with BrinValues flags in other places in patch 0003.
I added flag BrinOpcInfp.oi_regular_nulls which enables regular processing of
NULLs before calling of support functions. Now support functions don't need to
care about bv_hasnulls at all. add_value(), for example, works now only with
non-NULL values.

Patches 0002 and 0003 should be merged, I put 0003 in a separate patch just
for ease of review.

0004. BRIN bloom indexes
0005. BRIN multi-range minmax indexes

I have not looked carefully at these packs yet, but fixed only catalog entries
and removed NULLs processing according to patch 0003. I also noticed that the
following functions contain a lot of duplicated code, which needs to be
extracted into common subroutine:
inclusion_get_procinfo()
bloom_get_procinfo()
minmax_multi_get_procinfo()

Attached patches with all my changes.

--
Nikita Glukhov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-at-once-20190312.patch.gzapplication/gzip; name=0001-Pass-all-keys-to-BRIN-consistent-function-at-once-20190312.patch.gzDownload
0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20190312.patch.gzapplication/gzip; name=0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20190312.patch.gzDownload
0003-Move-processing-of-NULLs-from-BRIN-support-functions-20190312.patch.gzapplication/gzip; name=0003-Move-processing-of-NULLs-from-BRIN-support-functions-20190312.patch.gzDownload
0004-BRIN-bloom-indexes-20190312.patch.gzapplication/gzip; name=0004-BRIN-bloom-indexes-20190312.patch.gzDownload
0005-BRIN-multi-range-minmax-indexes-20190312.patch.gzapplication/gzip; name=0005-BRIN-multi-range-minmax-indexes-20190312.patch.gzDownload
#41Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Nikita Glukhov (#40)
Re: WIP: BRIN multi-range indexes

Hi Nikita,

Thanks for looking at the patch.

On 3/12/19 11:33 AM, Nikita Glukhov wrote:

Hi!

I have looked at this patch set too, but so far only at first two
infrastructure patches.

First of all, I agree that opclass parameters patch is needed here.

OK.

0001. Pass all keys to BRIN consistent function at once.

I think that changing the signature of consistent function is bad, because then
the authors of existing BRIN opclasses will need to maintain two variants of
the function for different version of PosgreSQL. Moreover, we can easily
distinguish two variants by the number of parameters. So I returned back a
call to old 3-argument variant of consistent() in bringetbitmap(). Also I
fixed brinvalidate() adding support for new 4-argument variant, and fixed
catalog entries for brin_minmax_consistent() and brin_inclusion_consistent()
which remained 3-argument. And also I removed unneeded indentation shift in
these two functions, which makes it difficult to compare changes, by extracting
subroutines minmax_consistent_key() and inclusion_consistent_key().

Hmmm. I admit I rather dislike functions that change the signature based
on the number of arguments, for some reason. But maybe it's better than
changing the consistent function. Not sure.

0002. Move IS NOT NULL checks to bringetbitmap()

I believe that removing duplicate code is always good. But in this case it
seems a bit inconsistent to refactor only bringetbitmap(). I think we can't
guarantee that existing opclasses work with null flags in add_value() and
union() in the expected way.

So I refactored the work with BrinValues flags in other places in patch 0003.
I added flag BrinOpcInfp.oi_regular_nulls which enables regular processing of
NULLs before calling of support functions. Now support functions don't need to
care about bv_hasnulls at all. add_value(), for example, works now only with
non-NULL values.

That seems like unnecessary complexity to me. We can't really guarantee
much about opclasses in extensions anyway. I don't know if there's some
sort of precedent but IMHO it's reasonable to expect the opclasses to be
updated accordingly.

Patches 0002 and 0003 should be merged, I put 0003 in a separate patch just
for ease of review.

Thanks.

0004. BRIN bloom indexes
0005. BRIN multi-range minmax indexes

I have not looked carefully at these packs yet, but fixed only catalog entries
and removed NULLs processing according to patch 0003. I also noticed that the
following functions contain a lot of duplicated code, which needs to be
extracted into common subroutine:
inclusion_get_procinfo()
bloom_get_procinfo()
minmax_multi_get_procinfo()

Yes. The reason for the duplicate code is that initially this was
submitted as two separate patches, so there was no obvious need for
sharing code.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#42Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: Tomas Vondra (#41)
Re: WIP: BRIN multi-range indexes

On Tue, Mar 12, 2019 at 8:15 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

0001. Pass all keys to BRIN consistent function at once.

I think that changing the signature of consistent function is bad, because then
the authors of existing BRIN opclasses will need to maintain two variants of
the function for different version of PosgreSQL. Moreover, we can easily
distinguish two variants by the number of parameters. So I returned back a
call to old 3-argument variant of consistent() in bringetbitmap(). Also I
fixed brinvalidate() adding support for new 4-argument variant, and fixed
catalog entries for brin_minmax_consistent() and brin_inclusion_consistent()
which remained 3-argument. And also I removed unneeded indentation shift in
these two functions, which makes it difficult to compare changes, by extracting
subroutines minmax_consistent_key() and inclusion_consistent_key().

Hmmm. I admit I rather dislike functions that change the signature based
on the number of arguments, for some reason. But maybe it's better than
changing the consistent function. Not sure.

I also kind of dislike signature change based on the number of
arguments. But it's still good to let extensions use old interface if
needed. What do you think about invention new consistent method, so
that extension can implement one of them? We did similar thing for
GIN (bistate consistent vs tristate consistent).

------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#43Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alexander Korotkov (#42)
Re: WIP: BRIN multi-range indexes

On 3/13/19 9:15 AM, Alexander Korotkov wrote:

On Tue, Mar 12, 2019 at 8:15 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

0001. Pass all keys to BRIN consistent function at once.

I think that changing the signature of consistent function is bad, because then
the authors of existing BRIN opclasses will need to maintain two variants of
the function for different version of PosgreSQL. Moreover, we can easily
distinguish two variants by the number of parameters. So I returned back a
call to old 3-argument variant of consistent() in bringetbitmap(). Also I
fixed brinvalidate() adding support for new 4-argument variant, and fixed
catalog entries for brin_minmax_consistent() and brin_inclusion_consistent()
which remained 3-argument. And also I removed unneeded indentation shift in
these two functions, which makes it difficult to compare changes, by extracting
subroutines minmax_consistent_key() and inclusion_consistent_key().

Hmmm. I admit I rather dislike functions that change the signature based
on the number of arguments, for some reason. But maybe it's better than
changing the consistent function. Not sure.

I also kind of dislike signature change based on the number of
arguments. But it's still good to let extensions use old interface if
needed. What do you think about invention new consistent method, so
that extension can implement one of them? We did similar thing for
GIN (bistate consistent vs tristate consistent).

Possibly. The other annoyance of course is that to support the current
consistent method we'll have to keep all the code I guess :-(

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#44Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: Tomas Vondra (#43)
Re: WIP: BRIN multi-range indexes

On Wed, Mar 13, 2019 at 12:52 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On 3/13/19 9:15 AM, Alexander Korotkov wrote:

On Tue, Mar 12, 2019 at 8:15 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

0001. Pass all keys to BRIN consistent function at once.

I think that changing the signature of consistent function is bad, because then
the authors of existing BRIN opclasses will need to maintain two variants of
the function for different version of PosgreSQL. Moreover, we can easily
distinguish two variants by the number of parameters. So I returned back a
call to old 3-argument variant of consistent() in bringetbitmap(). Also I
fixed brinvalidate() adding support for new 4-argument variant, and fixed
catalog entries for brin_minmax_consistent() and brin_inclusion_consistent()
which remained 3-argument. And also I removed unneeded indentation shift in
these two functions, which makes it difficult to compare changes, by extracting
subroutines minmax_consistent_key() and inclusion_consistent_key().

Hmmm. I admit I rather dislike functions that change the signature based
on the number of arguments, for some reason. But maybe it's better than
changing the consistent function. Not sure.

I also kind of dislike signature change based on the number of
arguments. But it's still good to let extensions use old interface if
needed. What do you think about invention new consistent method, so
that extension can implement one of them? We did similar thing for
GIN (bistate consistent vs tristate consistent).

Possibly. The other annoyance of course is that to support the current
consistent method we'll have to keep all the code I guess :-(

Yes, because incompatible change of opclass support function signature
is the thing we never did before. We have to add new optional
arguments to GiST functions, but that was compatible change.

If we do incompatible change of opclass interface, it becomes unclear
to do pg_upgrade with extension installed. Imagine, if we don't
require function signature to match, we could easily get segfault
because of extension incompatibility. If we do require function
signature to match, extension upgrade would become complex. It would
be required to not only adjust C-code, but also write some custom
script, which changes opclass (and users would have to run this script
manually?).

------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#45Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alexander Korotkov (#38)
10 attachment(s)
Re: WIP: BRIN multi-range indexes

On Sun, Mar 03, 2019 at 07:29:26AM +0300, Alexander Korotkov wrote:

On Sun, Mar 3, 2019 at 12:25 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

I've looked at that patch only very briefly so far, but I agree it's
likely a better solution than what my patch does at the moment (which I
agree is a misuse of the AM-level options). I'll take a closer look.

I agree it makes sense to re-use that infrastructure for this patch, but
I'm hesitant to rebase it on top of that patch right away. Because it
would mean this thread dependent on it, which would confuse cputube,
make it bitrot faster etc.

So I suggest we ignore this aspect of the patch for now, and let's talk
about the other bits first.

Works for me. We don't need to make the whole work made by this patch
to be dependent on opclass parameters. It's OK to ignore this aspect
for now and come back when opclass parameters get committed.

Attached is this patch series, rebased on top of current master and the
opclass parameters patch [1]/messages/by-id/d22c3a18-31c7-1879-fc11-4c1ce2f5e5af@postgrespro.ru. I previously planned to keep those two
efforts separate for a while, but I decided to give it a try and the
breakage is fairly minor so I'll keep it this way - this patch has zero
chance of getting committed with the opclass parameters patch anyway.

Aside from rebase and changes due to adopting opclass parameters, the
patch is otherwise unchanged.

0001-0004 are just the opclass parameters patch series.

0005 adds opclass parameters to BRIN indexes (similarly to what the
preceding parts to for GIN/GiST indexes).

0006-0010 are the original patch series (BRIN tweaks, bloom and
multi-minmax) rebased and switched to opclass parameters.

So now, we can do things like this:

CREATE INDEX x ON t USING brin (
col1 int4_bloom_ops(false_positive_rate = 0.05),
col2 int4_minmax_multi_ops(values_per_range = 16)
) WITH (pages_per_range = 32);

and so on. I think the patch [1]/messages/by-id/d22c3a18-31c7-1879-fc11-4c1ce2f5e5af@postgrespro.ru works fine - I only have some minor
comments, that I'll post to that thread.

The other challenges (e.g. how to pick the values for opclass parameters
automatically, based on the data) are still open.

regards

[1]: /messages/by-id/d22c3a18-31c7-1879-fc11-4c1ce2f5e5af@postgrespro.ru

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0009-BRIN-bloom-indexes-20190611.patchtext/plain; charset=us-asciiDownload
From c6e551147563a092a4fd003d4e497cfaf4dcced1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:09:09 +0200
Subject: [PATCH 09/10] BRIN bloom indexes

---
 doc/src/sgml/brin.sgml                   | 215 ++++++
 doc/src/sgml/ref/create_index.sgml       |  31 +
 src/backend/access/brin/Makefile         |   2 +-
 src/backend/access/brin/brin.c           |  22 +-
 src/backend/access/brin/brin_bloom.c     | 858 +++++++++++++++++++++++
 src/include/access/brin.h                |   2 +
 src/include/access/brin_internal.h       |   4 +
 src/include/catalog/pg_amop.dat          | 165 +++++
 src/include/catalog/pg_amproc.dat        | 430 ++++++++++++
 src/include/catalog/pg_opclass.dat       |  69 ++
 src/include/catalog/pg_opfamily.dat      |  36 +
 src/include/catalog/pg_proc.dat          |  20 +
 src/test/regress/expected/brin_bloom.out | 456 ++++++++++++
 src/test/regress/expected/opr_sanity.out |   3 +-
 src/test/regress/parallel_schedule       |   5 +
 src/test/regress/serial_schedule         |   1 +
 src/test/regress/sql/brin_bloom.sql      | 404 +++++++++++
 17 files changed, 2715 insertions(+), 8 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index da0c911153..b73f0b7d2b 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -129,6 +129,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry><literal>int8_bloom_ops</literal></entry>
+     <entry><type>bigint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int8_minmax_ops</literal></entry>
      <entry><type>bigint</type></entry>
@@ -180,6 +187,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>|&amp;&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>bytea_bloom_ops</literal></entry>
+     <entry><type>bytea</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>bytea_minmax_ops</literal></entry>
      <entry><type>bytea</type></entry>
@@ -191,6 +205,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>bpchar_bloom_ops</literal></entry>
+     <entry><type>character</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>bpchar_minmax_ops</literal></entry>
      <entry><type>character</type></entry>
@@ -202,6 +223,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>char_bloom_ops</literal></entry>
+     <entry><type>"char"</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>char_minmax_ops</literal></entry>
      <entry><type>"char"</type></entry>
@@ -213,6 +241,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>date_bloom_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>date_minmax_ops</literal></entry>
      <entry><type>date</type></entry>
@@ -224,6 +259,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float8_bloom_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float8_minmax_ops</literal></entry>
      <entry><type>double precision</type></entry>
@@ -235,6 +277,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>inet_bloom_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>inet_minmax_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -258,6 +307,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&lt;&lt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>int4_bloom_ops</literal></entry>
+     <entry><type>integer</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int4_minmax_ops</literal></entry>
      <entry><type>integer</type></entry>
@@ -269,6 +325,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>interval_bloom_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>interval_minmax_ops</literal></entry>
      <entry><type>interval</type></entry>
@@ -280,6 +343,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr_bloom_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr_minmax_ops</literal></entry>
      <entry><type>macaddr</type></entry>
@@ -291,6 +361,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr8_minmax_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
@@ -302,6 +379,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>name_bloom_ops</literal></entry>
+     <entry><type>name</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>name_minmax_ops</literal></entry>
      <entry><type>name</type></entry>
@@ -313,6 +397,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>numeric_bloom_ops</literal></entry>
+     <entry><type>numeric</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>numeric_minmax_ops</literal></entry>
      <entry><type>numeric</type></entry>
@@ -324,6 +415,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><type>pg_lsn</type></entry>
@@ -335,6 +433,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>oid_bloom_ops</literal></entry>
+     <entry><type>oid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>oid_minmax_ops</literal></entry>
      <entry><type>oid</type></entry>
@@ -366,6 +471,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;=</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float4_bloom_ops</literal></entry>
+     <entry><type>real</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float4_minmax_ops</literal></entry>
      <entry><type>real</type></entry>
@@ -377,6 +489,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>int2_bloom_ops</literal></entry>
+     <entry><type>smallint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int2_minmax_ops</literal></entry>
      <entry><type>smallint</type></entry>
@@ -388,6 +507,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>text_bloom_ops</literal></entry>
+     <entry><type>text</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>text_minmax_ops</literal></entry>
      <entry><type>text</type></entry>
@@ -410,6 +536,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamp_bloom_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamp_minmax_ops</literal></entry>
      <entry><type>timestamp without time zone</type></entry>
@@ -421,6 +554,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamptz_minmax_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
@@ -432,6 +572,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>time_bloom_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>time_minmax_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
@@ -443,6 +590,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timetz_bloom_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timetz_minmax_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
@@ -454,6 +608,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>uuid_bloom_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>uuid_minmax_ops</literal></entry>
      <entry><type>uuid</type></entry>
@@ -803,6 +964,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 61401f3645..05b2b01614 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -523,6 +523,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 5aef925ed4..a76d927ee0 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = brin.o brin_pageops.o brin_revmap.o brin_tuple.o brin_xlog.o \
-       brin_minmax.o brin_inclusion.o brin_validate.o
+       brin_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 67f42ed5d1..8878bf10f7 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -53,6 +53,7 @@ typedef struct BrinBuildState
 	BrinRevmap *bs_rmAccess;
 	BrinDesc   *bs_bdesc;
 	BrinMemTuple *bs_dtuple;
+	bytea     **opclassOptions;
 } BrinBuildState;
 
 /*
@@ -77,7 +78,8 @@ static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
 static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
-					BrinMemTuple *dtup, Datum *values, bool *nulls);
+					BrinMemTuple *dtup, Datum *values, bool *nulls,
+					bytea **options);
 static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
@@ -238,7 +240,8 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
+		/* FIXME use correct opclass options */
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls, NULL);
 
 		if (!need_insert)
 		{
@@ -736,7 +739,7 @@ brinbuildCallback(Relation index,
 
 	/* Accumulate the current tuple into the running state */
 	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
-							   values, isnull);
+							   values, isnull, state->opclassOptions);
 }
 
 /*
@@ -1218,6 +1221,7 @@ initialize_brin_buildstate(Relation idxRel, BrinRevmap *revmap,
 	state->bs_rmAccess = revmap;
 	state->bs_bdesc = brin_build_desc(idxRel);
 	state->bs_dtuple = brin_new_memtuple(state->bs_bdesc);
+	state->opclassOptions = RelationGetParsedOpclassOptions(idxRel);
 
 	brin_memtuple_initialize(state->bs_dtuple, state->bs_bdesc);
 
@@ -1250,6 +1254,7 @@ terminate_brin_buildstate(BrinBuildState *state)
 
 	brin_free_desc(state->bs_bdesc);
 	pfree(state->bs_dtuple);
+	pfree(state->opclassOptions);
 	pfree(state);
 }
 
@@ -1626,7 +1631,7 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 
 static bool
 add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
-					Datum *values, bool *nulls)
+					Datum *values, bool *nulls, bytea **options)
 {
 	int			keyno;
 	bool		modified = false;
@@ -1643,6 +1648,7 @@ add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
 		Datum		result;
 		BrinValues *bval;
 		FmgrInfo   *addValue;
+		bytea	   *opts;
 
 		bval = &dtup->bt_columns[keyno];
 
@@ -1661,14 +1667,18 @@ add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
 			continue;
 		}
 
+		opts = (options) ? options[keyno] : NULL;
+
+
 		addValue = index_getprocinfo(idxRel, keyno + 1,
 									 BRIN_PROCNUM_ADDVALUE);
-		result = FunctionCall4Coll(addValue,
+		result = FunctionCall5Coll(addValue,
 								   idxRel->rd_indcollation[keyno],
 								   PointerGetDatum(bdesc),
 								   PointerGetDatum(bval),
 								   values[keyno],
-								   nulls[keyno]);
+								   nulls[keyno],
+								   PointerGetDatum(opts));
 		/* if that returned true, we need to insert the updated tuple */
 		modified |= DatumGetBool(result);
 	}
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..ef026a72d5
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,858 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		4	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+#define		BLOOM_PHASE_SORTED		1
+#define		BLOOM_PHASE_HASH		2
+
+/* how many hashes to accumulate before hashing */
+#define		BLOOM_MAX_UNSORTED		32
+#define		BLOOM_GROW_BYTES		32
+
+/*
+ * Storage type for BRIN's reloptions
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/* default values for bloom-specific reloptions */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		128
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Maybe instead of explicitly limiting the number of unsorted values
+ * by BLOOM_MAX_UNSORTED, we should cap them by (filter size / 4B), i.e.
+ * allow up to the whole filter size.
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* global bloom filter parameters */
+	uint32	phase;		/* phase (initially SORTED, then HASH) */
+	uint32	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (optimal) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* fields used only in the EXACT phase */
+	uint32	nvalues;	/* number of hashes stored (sorted + extra) */
+	uint32	nsorted;	/* number of uint32 hashes in sorted part */
+
+	/* bitmap of the bloom filter */
+	char 	bitmap[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	/* https://en.wikipedia.org/wiki/Bloom_filter */
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 */
+	len = Max(offsetof(BloomFilter, bitmap), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->phase = BLOOM_PHASE_SORTED;
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * XXX Actually, we don't need to do repalloc - we just need to set the
+ * varlena header length!
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i,
+			nvalues;
+	uint32 *values;
+
+	/* never call compact on filters in HASH phase */
+	Assert(filter->phase == BLOOM_PHASE_SORTED);
+
+	/* no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) palloc(filter->nvalues * sizeof(uint32));
+
+	/* copy the data, then reset the bitmap */
+	memcpy(values, filter->bitmap, filter->nvalues * sizeof(uint32));
+	memset(filter->bitmap, 0, filter->nvalues * sizeof(uint32));
+
+	/* FIXME optimization: sort only the unsorted part, then merge */
+	pg_qsort(values, filter->nvalues, sizeof(uint32), cmp_uint32);
+
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if a new value, keep it */
+		if (values[i] != values[i-1])
+		{
+			values[nvalues] = values[i];
+			nvalues++;
+		}
+	}
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	memcpy(filter->bitmap, values, nvalues * sizeof(uint32));
+
+	pfree(values);
+}
+
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter);
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (filter->phase == BLOOM_PHASE_SORTED)
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter,bitmap) + (filter->nvalues+1) * sizeof(uint32);
+
+			if (len < need)
+			{
+				/*
+				 * We don't double the size here, as in the first place we care about
+				 * reducing storage requirements, and the doubling happens automatically
+				 * in memory contexts anyway.
+				 *
+				 * XXX Zero the newly allocated part. Maybe not really needed?
+				 */
+				filter = (BloomFilter *) repalloc(filter, len + BLOOM_GROW_BYTES);
+				memset((char *)filter + len, 0, BLOOM_GROW_BYTES);
+				SET_VARSIZE(filter, len + BLOOM_GROW_BYTES);
+			}
+
+			/* copy the data into the bitmap */
+			memcpy(&filter->bitmap[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(filter->phase == BLOOM_PHASE_HASH);
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32)DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->bitmap[byte] & (0x01 << bit)))
+		{
+			filter->bitmap[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, bitmap) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->phase = BLOOM_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->bitmap;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (filter->phase == BLOOM_PHASE_SORTED)
+	{
+		int i;
+		uint32 *values = (uint32 *)filter->bitmap;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+			if (value == values[i])
+				return true;
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(filter->phase == BLOOM_PHASE_HASH);
+
+	big_h = ((uint32)DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->bitmap[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store a the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct for the column(s) and
+ * compute the expected number of distinct values in a range. But that
+ * may be tricky due to data being sorted in various ways, so it seems
+ * better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unnecessarily small bloom filters.
+	 * We never use less than 128, because we would not save much space
+	 * anyway. MaxHeapTuplesPerPage is ~290, so this is still lower.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GETARG_POINTER(4);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// BloomOptions *opts = (BloomOptions *) PG_GETARG_POINTER(4);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// BloomOptions *opts = (BloomOptions *) PG_GETARG_POINTER(3);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (filter_b->phase == BLOOM_PHASE_SORTED)
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->bitmap;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (filter_a->phase == BLOOM_PHASE_SORTED)
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->bitmap;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (filter_a->phase == BLOOM_PHASE_HASH)
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->bitmap[i] |= filter_b->bitmap[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	Datum		raw_options = PG_GETARG_DATUM(0);
+	bool		validate = PG_GETARG_BOOL(1);
+	relopt_real	opts[] = {
+		{
+			{"n_distinct_per_range", "xxx", 0, 0, 20, RELOPT_TYPE_REAL },
+			-0.1, -1.0, INT_MAX
+		},
+		{
+			{"false_positive_rate", "xxx", 0, 0, 19, RELOPT_TYPE_REAL },
+			0.01, 0.001, 1.0
+		}
+	};
+
+	relopt_gen *optgen[] = { &opts[0].gen, &opts[1].gen };
+	int			offsets[] = { offsetof(BloomOptions, nDistinctPerRange),
+							  offsetof(BloomOptions, falsePositiveRate)};
+
+	BloomOptions *options =
+		parseAndFillLocalRelOptions(raw_options, optgen, offsets, 2,
+									sizeof(BloomOptions), validate);
+
+	PG_RETURN_POINTER(options);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 8271404332..37cf154e91 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -23,6 +23,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 21a79803b3..633e1ec90e 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index cf63eb7d54..90ae219598 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1675,6 +1675,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1692,6 +1697,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1709,6 +1719,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1855,6 +1870,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1872,6 +1925,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1889,6 +1947,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1972,6 +2035,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -1989,6 +2066,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2006,6 +2088,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2023,6 +2110,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2060,6 +2152,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2077,6 +2174,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2223,6 +2325,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2240,6 +2380,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2257,6 +2402,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2308,6 +2458,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2325,6 +2480,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2386,6 +2546,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5ceee11ab1..c7199f0a91 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -697,6 +697,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -710,6 +728,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -723,6 +759,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -824,6 +878,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -837,6 +943,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -849,6 +973,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -911,6 +1052,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -925,6 +1105,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -939,6 +1139,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -952,6 +1172,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -986,6 +1224,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -999,6 +1257,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1106,6 +1382,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1120,6 +1452,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1134,6 +1486,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1174,6 +1546,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1187,6 +1579,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1222,6 +1632,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index fdfea85efe..ebc35fd98c 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -250,67 +250,127 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -319,18 +379,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 41e40d657a..fd3d659b68 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -176,50 +176,86 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '5024',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '5027',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '5045',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '5042',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '5038',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '5022',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '5048',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '5023',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '5049',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '5050',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '5033',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '5034',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '5035',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '5036',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '5037',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '5041',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '5046',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '5047',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3fb4073367..0a1a83807b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7914,6 +7914,26 @@
   proargtypes => 'internal internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '5039', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '5040', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '5043', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4 internal',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '5044', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '6016', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'internal',
+  proargtypes => 'internal bool', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'u',
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 85af36ee5b..400f333ef2 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -1924,6 +1924,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -1987,7 +1988,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f23fe8d870..38a18660da 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom 
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index ca200eb599..56fb53d053 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -105,6 +105,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.20.1

0010-BRIN-multi-range-minmax-indexes-20190611.patchtext/plain; charset=us-asciiDownload
From 47999d4fe405860de81087b05931e5a617f66289 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:10:50 +0200
Subject: [PATCH 10/10] BRIN multi-range minmax indexes

---
 doc/src/sgml/brin.sgml                      |  212 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    3 +-
 src/backend/access/brin/brin_minmax_multi.c | 2187 +++++++++++++++++++
 src/backend/access/index/indexam.c          |    3 +-
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/transam.h                |    8 +-
 src/include/catalog/pg_amop.dat             |  545 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   30 +-
 src/include/catalog/pg_proc.dat             |   65 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 ++++
 17 files changed, 4504 insertions(+), 16 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index b73f0b7d2b..9cdcb4652d 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -259,6 +259,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>date_minmax_multi_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float8_bloom_ops</literal></entry>
      <entry><type>double precision</type></entry>
@@ -277,6 +288,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>inet_bloom_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -295,6 +317,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>network_inclusion_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -343,6 +376,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr_bloom_ops</literal></entry>
      <entry><type>macaddr</type></entry>
@@ -361,6 +405,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr8_bloom_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
@@ -379,6 +434,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>name_bloom_ops</literal></entry>
      <entry><type>name</type></entry>
@@ -433,6 +499,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>oid_bloom_ops</literal></entry>
      <entry><type>oid</type></entry>
@@ -554,6 +631,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamptz_bloom_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
@@ -572,6 +660,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>time_bloom_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
@@ -590,6 +689,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>time_minmax_multi_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timetz_bloom_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
@@ -608,6 +718,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>uuid_bloom_ops</literal></entry>
      <entry><type>uuid</type></entry>
@@ -626,6 +747,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
    </tbody>
   </tgroup>
  </table>
@@ -720,13 +852,13 @@ typedef struct BrinOpcInfo
    </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions using
+  them are shipped for in-core data types as appropriate.  Additional operator
+  classes can be defined by the user for other data types using equivalent
+  definitions, without having to write any source code; appropriate catalog
+  entries being declared is enough.  Note that assumptions about the semantics
+  of operator strategies are embedded in the support functions' source code.
  </para>
 
  <para>
@@ -795,6 +927,72 @@ typedef struct BrinOpcInfo
   </tgroup>
  </table>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
   To write an operator class for a complex data type which has values
   included within another type, it's possible to use the inclusion support
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 05b2b01614..d1f3ab1102 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -554,6 +554,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 64.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index a76d927ee0..c87c796472 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -13,6 +13,7 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = brin.o brin_pageops.o brin_revmap.o brin_tuple.o brin_xlog.o \
-       brin_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o
+       brin_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o \
+       brin_minmax_multi.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..d4d97823ce
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2187 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+static Ranges *range_deserialize(SerializedRanges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (attr->attlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (attr->attlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(attr->attlen > 0);
+		len += nvalues * attr->attlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized,
+				AttrNumber attno, Form_pg_attribute attr)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTLessStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count_values(cranges, ncranges) <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		FmgrInfo   *cmpFn;
+		Datum	   *values;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												   BTLessStrategyNumber);
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(bdesc->bd_index);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GETARG_POINTER(4);
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		column->bv_allnulls = false;
+		modified = true;
+	}
+	else
+	{
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized, attno, attr);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+	if (modified)
+	{
+		SerializedRanges *s = range_serialize(ranges, attno, attr);
+		column->bv_values[0] = PointerGetDatum(s);
+
+		/*
+		 * XXX pfree must happen after range_serialize, because the Ranges value
+		 * may reference the original serialized value.
+		 */
+		if (serialized)
+			pfree(serialized);
+	}
+
+	pfree(ranges);
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GETARG_POINTER(4);
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+	Form_pg_attribute attr;
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized, attno, attr);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GETARG_POINTER(3);
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a, attno, attr);
+	ranges_b = range_deserialize(serialized_b, attno, attr);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attno, attr));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	Datum		raw_options = PG_GETARG_DATUM(0);
+	bool		validate = PG_GETARG_BOOL(1);
+	relopt_int	opts[] = {
+		{
+			{"values_per_range", "xxx", 0, 0, 16, RELOPT_TYPE_INT },
+			0, 16, 256
+		}
+	};
+
+	relopt_gen *optgen[] = { &opts[0].gen };
+	int			offsets[] = { offsetof(MinMaxOptions, valuesPerRange)};
+
+	MinMaxOptions *options =
+		parseAndFillLocalRelOptions(raw_options, optgen, offsets, 1,
+									sizeof(MinMaxOptions), validate);
+
+	PG_RETURN_POINTER(options);
+}
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 6cc76914de..8e74684050 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -974,10 +974,11 @@ index_opclass_options_generic(Relation indrel, AttrNumber attnum,
 		initStringInfo(&opclassname);
 		get_opclass_name(opclass, InvalidOid, &opclassname);
 
+		/* XXX get_opclass_name prepends the name with a space */
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("operator class \"%s\" has no options",
-						opclassname.data)));
+						&opclassname.data[1])));
 	}
 
 	procinfo = index_getprocinfo(indrel, attnum, procnum);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 37cf154e91..6698ec688d 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -25,6 +25,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 633e1ec90e..cc9a728815 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 6cbb0c82c7..f95caf6c1c 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -116,14 +116,14 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  *		when the .dat files in src/include/catalog/ do not specify an OID
  *		for a catalog entry that requires one.
  *
- *		OIDS 12000-16383 are reserved for assignment during initdb
- *		using the OID generator.  (We start the generator at 12000.)
+ *		OIDS 13000-16383 are reserved for assignment during initdb
+ *		using the OID generator.  (We start the generator at 13000.)
  *
  *		OIDs beginning at 16384 are assigned from the OID generator
  *		during normal multiuser operation.  (We force the generator up to
  *		16384 as soon as we are in normal operation.)
  *
- * The choices of 8000, 10000 and 12000 are completely arbitrary, and can be
+ * The choices of 8000, 10000 and 13000 are completely arbitrary, and can be
  * moved if we run low on OIDs in any category.  Changing the macros below,
  * and updating relevant documentation (see bki.sgml and RELEASE_CHANGES),
  * should be sufficient to do this.  Moving the 16384 boundary between
@@ -137,7 +137,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 90ae219598..2f5b538f58 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1870,6 +1870,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1947,6 +2093,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -1969,6 +2132,23 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2035,6 +2215,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2066,6 +2312,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2088,6 +2351,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2110,6 +2390,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2174,6 +2471,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2325,6 +2639,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2380,6 +2840,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2402,6 +2879,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2458,6 +2952,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2480,6 +2991,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2546,6 +3074,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index c7199f0a91..ea82f8e1e4 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -878,6 +878,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -973,6 +1119,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1002,6 +1165,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1052,6 +1232,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1105,6 +1359,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1139,6 +1413,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1172,6 +1466,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1257,6 +1571,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1382,6 +1715,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1452,6 +1949,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1486,6 +2003,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1546,6 +2083,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1577,7 +2134,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1632,6 +2210,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index ebc35fd98c..32c3ed7988 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -268,18 +268,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -291,38 +300,59 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -338,36 +368,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -379,6 +427,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -388,6 +439,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -397,6 +451,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index fd3d659b68..f3e0936a67 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -176,10 +176,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '5054',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '5024',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '5087',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '5027',
@@ -188,10 +192,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '5059',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '5042',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '5088',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '5038',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -209,23 +217,35 @@
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
 { oid => '5049',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
+{ oid => '5051',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '5089',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
-{ oid => '5050',
+{ oid => '5091',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
+{ oid => '5052',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '5064',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '5033',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '5063',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '5034',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '5066',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '5035',
@@ -236,10 +256,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '5090',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '5037',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '5061',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '5041',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -248,12 +272,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '5050',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '5046',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '5065',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '5047',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0a1a83807b..d3d874dd52 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7897,6 +7897,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '5068', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '5069', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '5070', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4 internal',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '5071', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '6015', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'internal',
+  proargtypes => 'internal bool', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '5072', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '5073', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '5074', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '5075', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '5076', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '5077', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '5078', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '5079', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '5080', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '5081', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '5082', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '5083', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '5084', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '5085', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '5086', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 38a18660da..ab92ea8a13 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom 
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 56fb53d053..034030d866 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -106,6 +106,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.20.1

0001-Add-opclass-parameters-20190611.patchtext/plain; charset=us-asciiDownload
From 436eca7a1dd45b78b50c3868fd4be518b814cecd Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 20:59:01 +0200
Subject: [PATCH 01/10] Add opclass parameters

---
 doc/src/sgml/indices.sgml                 |   2 +-
 doc/src/sgml/ref/create_index.sgml        |  16 ++-
 src/backend/access/common/reloptions.c    | 142 +++++++++++++++-------
 src/backend/access/index/indexam.c        |  81 ++++++++++++
 src/backend/catalog/heap.c                |   8 +-
 src/backend/catalog/index.c               |  23 +++-
 src/backend/catalog/toasting.c            |   1 +
 src/backend/commands/indexcmds.c          |  17 ++-
 src/backend/commands/tablecmds.c          |   2 +-
 src/backend/nodes/copyfuncs.c             |   1 +
 src/backend/nodes/equalfuncs.c            |   1 +
 src/backend/nodes/outfuncs.c              |   1 +
 src/backend/optimizer/util/plancat.c      |   4 +
 src/backend/parser/gram.y                 |  72 +++++++----
 src/backend/utils/adt/ruleutils.c         | 128 +++++++++++--------
 src/backend/utils/cache/relcache.c        |  99 +++++++++++++++
 src/include/access/amapi.h                |   7 ++
 src/include/access/genam.h                |   5 +
 src/include/access/reloptions.h           |   5 +
 src/include/catalog/heap.h                |   1 +
 src/include/nodes/execnodes.h             |   2 +
 src/include/nodes/parsenodes.h            |   1 +
 src/include/nodes/pathnodes.h             |   1 +
 src/include/utils/rel.h                   |   1 +
 src/include/utils/relcache.h              |   3 +
 src/include/utils/ruleutils.h             |   2 +
 src/test/regress/expected/btree_index.out |   5 +
 src/test/regress/sql/btree_index.sql      |   4 +
 28 files changed, 503 insertions(+), 132 deletions(-)

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 95c0a1926c..ea3acea88e 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1253,7 +1253,7 @@ SELECT target FROM tests WHERE subject = 'some-subject' AND success;
    An index definition can specify an <firstterm>operator
    class</firstterm> for each column of an index.
 <synopsis>
-CREATE INDEX <replaceable>name</replaceable> ON <replaceable>table</replaceable> (<replaceable>column</replaceable> <replaceable>opclass</replaceable> <optional><replaceable>sort options</replaceable></optional> <optional>, ...</optional>);
+CREATE INDEX <replaceable>name</replaceable> ON <replaceable>table</replaceable> (<replaceable>column</replaceable> <replaceable>opclass</replaceable> [ ( <replaceable>opclass_options</replaceable> ) ] <optional><replaceable>sort options</replaceable></optional> <optional>, ...</optional>);
 </synopsis>
    The operator class identifies the operators to be used by the index
    for that column.  For example, a B-tree index on the type <type>int4</type>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 629a31ef79..61401f3645 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> ] ON [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ USING <replaceable class="parameter">method</replaceable> ]
-    ( { <replaceable class="parameter">column_name</replaceable> | ( <replaceable class="parameter">expression</replaceable> ) } [ COLLATE <replaceable class="parameter">collation</replaceable> ] [ <replaceable class="parameter">opclass</replaceable> ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] )
+    ( { <replaceable class="parameter">column_name</replaceable> | ( <replaceable class="parameter">expression</replaceable> ) } [ COLLATE <replaceable class="parameter">collation</replaceable> ] { <replaceable class="parameter">opclass</replaceable> | DEFAULT } [ ( <replaceable class="parameter">opclass_parameter</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] )
     [ INCLUDE ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
@@ -278,6 +278,15 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><replaceable class="parameter">opclass_parameter</replaceable></term>
+      <listitem>
+       <para>
+        The name of an operator class parameter. See below for details.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><literal>ASC</literal></term>
       <listitem>
@@ -646,8 +655,9 @@ Indexes:
   </para>
 
   <para>
-   An <firstterm>operator class</firstterm> can be specified for each
-   column of an index. The operator class identifies the operators to be
+   An <firstterm>operator class</firstterm> with its optional parameters 
+   can be specified for each column of an index.
+   The operator class identifies the operators to be
    used by the index for that column. For example, a B-tree index on
    four-byte integers would use the <literal>int4_ops</literal> class;
    this operator class includes comparison functions for four-byte
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index de06c92574..abc0082ce7 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -1051,6 +1051,60 @@ extractRelOptions(HeapTuple tuple, TupleDesc tupdesc,
 	return options;
 }
 
+static void
+parseRelOptionsInternal(Datum options, bool validate,
+				 relopt_value *reloptions, int numoptions)
+{
+	ArrayType  *array = DatumGetArrayTypeP(options);
+	Datum	   *optiondatums;
+	int			noptions;
+	int			i;
+
+	deconstruct_array(array, TEXTOID, -1, false, 'i',
+					  &optiondatums, NULL, &noptions);
+
+	for (i = 0; i < noptions; i++)
+	{
+		char	   *text_str = VARDATA(optiondatums[i]);
+		int			text_len = VARSIZE(optiondatums[i]) - VARHDRSZ;
+		int			j;
+
+		/* Search for a match in reloptions */
+		for (j = 0; j < numoptions; j++)
+		{
+			int			kw_len = reloptions[j].gen->namelen;
+
+			if (text_len > kw_len && text_str[kw_len] == '=' &&
+				strncmp(text_str, reloptions[j].gen->name, kw_len) == 0)
+			{
+				parse_one_reloption(&reloptions[j], text_str, text_len,
+									validate);
+				break;
+			}
+		}
+
+		if (j >= numoptions && validate)
+		{
+			char	   *s;
+			char	   *p;
+
+			s = TextDatumGetCString(optiondatums[i]);
+			p = strchr(s, '=');
+			if (p)
+				*p = '\0';
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unrecognized parameter \"%s\"", s)));
+		}
+	}
+
+	/* It's worth avoiding memory leaks in this function */
+	pfree(optiondatums);
+
+	if (((void *) array) != DatumGetPointer(options))
+		pfree(array);
+}
+
 /*
  * Interpret reloptions that are given in text-array format.
  *
@@ -1105,57 +1159,61 @@ parseRelOptions(Datum options, bool validate, relopt_kind kind,
 
 	/* Done if no options */
 	if (PointerIsValid(DatumGetPointer(options)))
-	{
-		ArrayType  *array = DatumGetArrayTypeP(options);
-		Datum	   *optiondatums;
-		int			noptions;
+		parseRelOptionsInternal(options, validate, reloptions, numoptions);
 
-		deconstruct_array(array, TEXTOID, -1, false, 'i',
-						  &optiondatums, NULL, &noptions);
+	*numrelopts = numoptions;
+	return reloptions;
+}
 
-		for (i = 0; i < noptions; i++)
-		{
-			char	   *text_str = VARDATA(optiondatums[i]);
-			int			text_len = VARSIZE(optiondatums[i]) - VARHDRSZ;
-			int			j;
+/* Parse local unregistered options. */
+relopt_value *
+parseLocalRelOptions(Datum options, bool validate,
+					 relopt_gen *optgen[], int nopts)
+{
+	relopt_value *values = palloc(sizeof(*values) * nopts);
+	int			i;
 
-			/* Search for a match in reloptions */
-			for (j = 0; j < numoptions; j++)
-			{
-				int			kw_len = reloptions[j].gen->namelen;
+	for (i = 0; i < nopts; i++)
+	{
+		values[i].gen = optgen[i];
+		values[i].isset = false;
+	}
 
-				if (text_len > kw_len && text_str[kw_len] == '=' &&
-					strncmp(text_str, reloptions[j].gen->name, kw_len) == 0)
-				{
-					parse_one_reloption(&reloptions[j], text_str, text_len,
-										validate);
-					break;
-				}
-			}
+	if (options != (Datum) 0)
+		parseRelOptionsInternal(options, validate, values, nopts);
 
-			if (j >= numoptions && validate)
-			{
-				char	   *s;
-				char	   *p;
+	return values;
+}
 
-				s = TextDatumGetCString(optiondatums[i]);
-				p = strchr(s, '=');
-				if (p)
-					*p = '\0';
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						 errmsg("unrecognized parameter \"%s\"", s)));
-			}
-		}
+/*
+ * Parse local options, allocate a bytea struct that's of the specified
+ * 'base_size' plus any extra space that's needed for string variables,
+ * fill its option's fields located at the given offsets and return it.
+ */
+void *
+parseAndFillLocalRelOptions(Datum options, relopt_gen *optgen[], int offsets[],
+							int noptions, size_t base_size, bool validate)
+{
+	relopt_parse_elt *elems = palloc(sizeof(*elems) * noptions);
+	relopt_value *vals;
+	void	   *opts;
+	int			i;
 
-		/* It's worth avoiding memory leaks in this function */
-		pfree(optiondatums);
-		if (((void *) array) != DatumGetPointer(options))
-			pfree(array);
+	for (i = 0; i < noptions; i++)
+	{
+		elems[i].optname = optgen[i]->name;
+		elems[i].opttype = optgen[i]->type;
+		elems[i].offset = offsets[i];
 	}
 
-	*numrelopts = numoptions;
-	return reloptions;
+	vals = parseLocalRelOptions(options, validate, optgen, noptions);
+	opts = allocateReloptStruct(base_size, vals, noptions);
+	fillRelOptions(opts, base_size, vals, noptions, validate, elems, noptions);
+
+	if (elems)
+		pfree(elems);
+
+	return opts;
 }
 
 /*
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index aefdd2916d..6cc76914de 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -51,11 +51,14 @@
 #include "access/xlog.h"
 #include "catalog/index.h"
 #include "catalog/pg_type.h"
+#include "commands/defrem.h"
 #include "pgstat.h"
 #include "storage/bufmgr.h"
 #include "storage/lmgr.h"
 #include "storage/predicate.h"
+#include "utils/ruleutils.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 
 /* ----------------------------------------------------------------
@@ -905,3 +908,81 @@ index_store_float8_orderby_distances(IndexScanDesc scan, Oid *orderByTypes,
 		}
 	}
 }
+
+/* ----------------
+ *      index_opclass_options
+ *
+ *      Parse opclass-specific options for index column.
+ * ----------------
+ */
+bytea *
+index_opclass_options(Relation relation, AttrNumber attnum, Datum attoptions,
+					  bool validate)
+{
+	amopclassoptions_function amopclassoptions =
+		relation->rd_indam->amopclassoptions;
+
+	if (!amopclassoptions)
+	{
+		if (validate && PointerIsValid(DatumGetPointer(attoptions)))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("access method \"%s\" does not support opclass options ",
+							get_am_name(relation->rd_rel->relam))));
+
+		return NULL;
+	}
+
+	return amopclassoptions(relation, attnum, attoptions, validate);
+}
+
+/* ----------------
+ *      index_opclass_options_generic
+ *
+ *      Parse opclass options for index column using the specified support
+ *      function 'procnum' of column's opclass.
+ * ----------------
+ */
+bytea *
+index_opclass_options_generic(Relation indrel, AttrNumber attnum,
+							  uint16 procnum, Datum attoptions, bool validate)
+{
+	Oid			procid = index_getprocid(indrel, attnum, procnum);
+	FmgrInfo   *procinfo;
+
+	if (!OidIsValid(procid))
+	{
+		StringInfoData opclassname;
+		Oid			opclass;
+		Datum		indclassDatum;
+		oidvector  *indclass;
+		bool		isnull;
+
+		if (!DatumGetPointer(attoptions))
+			return NULL;	/* ok, no options, no procedure */
+
+		/*
+		 * Report an error if the opclass's options-parsing procedure does not
+		 * exist but the opclass options are specified.
+		 */
+		indclassDatum = SysCacheGetAttr(INDEXRELID, indrel->rd_indextuple,
+										Anum_pg_index_indclass, &isnull);
+		Assert(!isnull);
+		indclass = (oidvector *) DatumGetPointer(indclassDatum);
+		opclass = indclass->values[attnum - 1];
+
+		initStringInfo(&opclassname);
+		get_opclass_name(opclass, InvalidOid, &opclassname);
+
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("operator class \"%s\" has no options",
+						opclassname.data)));
+	}
+
+	procinfo = index_getprocinfo(indrel, attnum, procnum);
+
+	return (bytea *) DatumGetPointer(FunctionCall2(procinfo,
+												   attoptions,
+												   BoolGetDatum(validate)));
+}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 86820eecfc..4a42cb1523 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -694,6 +694,7 @@ CheckAttributeType(const char *attname,
 void
 InsertPgAttributeTuple(Relation pg_attribute_rel,
 					   Form_pg_attribute new_attribute,
+					   Datum attoptions,
 					   CatalogIndexState indstate)
 {
 	Datum		values[Natts_pg_attribute];
@@ -725,10 +726,11 @@ InsertPgAttributeTuple(Relation pg_attribute_rel,
 	values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(new_attribute->attislocal);
 	values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(new_attribute->attinhcount);
 	values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(new_attribute->attcollation);
+	values[Anum_pg_attribute_attoptions - 1] = attoptions;
 
 	/* start out with empty permissions and empty options */
 	nulls[Anum_pg_attribute_attacl - 1] = true;
-	nulls[Anum_pg_attribute_attoptions - 1] = true;
+	nulls[Anum_pg_attribute_attoptions - 1] = attoptions == (Datum) 0;
 	nulls[Anum_pg_attribute_attfdwoptions - 1] = true;
 	nulls[Anum_pg_attribute_attmissingval - 1] = true;
 
@@ -782,7 +784,7 @@ AddNewAttributeTuples(Oid new_rel_oid,
 		/* Make sure this is OK, too */
 		attr->attstattarget = -1;
 
-		InsertPgAttributeTuple(rel, attr, indstate);
+		InsertPgAttributeTuple(rel, attr, (Datum) 0, indstate);
 
 		/* Add dependency info */
 		myself.classId = RelationRelationId;
@@ -820,7 +822,7 @@ AddNewAttributeTuples(Oid new_rel_oid,
 			/* Fill in the correct relation OID in the copied tuple */
 			attStruct.attrelid = new_rel_oid;
 
-			InsertPgAttributeTuple(rel, &attStruct, indstate);
+			InsertPgAttributeTuple(rel, &attStruct, (Datum) 0, indstate);
 		}
 	}
 
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index d2e4f53a80..125174852b 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -26,6 +26,7 @@
 #include "access/amapi.h"
 #include "access/heapam.h"
 #include "access/multixact.h"
+#include "access/reloptions.h"
 #include "access/relscan.h"
 #include "access/sysattr.h"
 #include "access/tableam.h"
@@ -106,7 +107,8 @@ static TupleDesc ConstructTupleDescriptor(Relation heapRelation,
 										  Oid *classObjectId);
 static void InitializeAttributeOids(Relation indexRelation,
 									int numatts, Oid indexoid);
-static void AppendAttributeTuples(Relation indexRelation, int numatts);
+static void AppendAttributeTuples(Relation indexRelation, int numatts,
+					  Datum *attopts);
 static void UpdateIndexRelation(Oid indexoid, Oid heapoid,
 								Oid parentIndexId,
 								IndexInfo *indexInfo,
@@ -486,7 +488,7 @@ InitializeAttributeOids(Relation indexRelation,
  * ----------------------------------------------------------------
  */
 static void
-AppendAttributeTuples(Relation indexRelation, int numatts)
+AppendAttributeTuples(Relation indexRelation, int numatts, Datum *attopts)
 {
 	Relation	pg_attribute;
 	CatalogIndexState indstate;
@@ -508,10 +510,11 @@ AppendAttributeTuples(Relation indexRelation, int numatts)
 	for (i = 0; i < numatts; i++)
 	{
 		Form_pg_attribute attr = TupleDescAttr(indexTupDesc, i);
+		Datum		attoptions = attopts ? attopts[i] : (Datum) 0;
 
 		Assert(attr->attnum == i + 1);
 
-		InsertPgAttributeTuple(pg_attribute, attr, indstate);
+		InsertPgAttributeTuple(pg_attribute, attr, attoptions, indstate);
 	}
 
 	CatalogCloseIndexes(indstate);
@@ -591,6 +594,7 @@ UpdateIndexRelation(Oid indexoid,
 	else
 		predDatum = (Datum) 0;
 
+
 	/*
 	 * open the system catalog index relation
 	 */
@@ -933,7 +937,8 @@ index_create(Relation heapRelation,
 	/*
 	 * append ATTRIBUTE tuples for the index
 	 */
-	AppendAttributeTuples(indexRelation, indexInfo->ii_NumIndexAttrs);
+	AppendAttributeTuples(indexRelation, indexInfo->ii_NumIndexAttrs,
+						  indexInfo->ii_OpclassOptions);
 
 	/* ----------------
 	 *	  update pg_index
@@ -1146,6 +1151,12 @@ index_create(Relation heapRelation,
 
 	indexRelation->rd_index->indnkeyatts = indexInfo->ii_NumIndexKeyAttrs;
 
+	/* Validate opclass-specific options */
+	if (indexInfo->ii_OpclassOptions)
+		for (i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++)
+			(void) index_opclass_options(indexRelation, i + 1,
+										 indexInfo->ii_OpclassOptions[i], true);
+
 	/*
 	 * If this is bootstrap (initdb) time, then we don't actually fill in the
 	 * index yet.  We'll be creating more indexes and classes later, so we
@@ -2208,6 +2219,10 @@ BuildIndexInfo(Relation index)
 		ii->ii_ExclusionStrats = NULL;
 	}
 
+	ii->ii_OpclassOptions =
+		RelationGetRawOpclassOptions(RelationGetRelid(index),
+									 RelationGetNumberOfAttributes(index));
+
 	/* other info */
 	ii->ii_Unique = indexStruct->indisunique;
 	ii->ii_ReadyForInserts = indexStruct->indisready;
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index de6282a667..7290731b3d 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -304,6 +304,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	indexInfo->ii_ExclusionOps = NULL;
 	indexInfo->ii_ExclusionProcs = NULL;
 	indexInfo->ii_ExclusionStrats = NULL;
+	indexInfo->ii_OpclassOptions = NULL;
 	indexInfo->ii_Unique = true;
 	indexInfo->ii_ReadyForInserts = true;
 	indexInfo->ii_Concurrent = false;
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index d05d2fd3d5..23a57be749 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -792,6 +792,7 @@ DefineIndex(Oid relationId,
 	indexInfo->ii_ExclusionOps = NULL;
 	indexInfo->ii_ExclusionProcs = NULL;
 	indexInfo->ii_ExclusionStrats = NULL;
+	indexInfo->ii_OpclassOptions = NULL;	/* for now */
 	indexInfo->ii_Unique = stmt->unique;
 	/* In a concurrent build, mark it not-ready-for-inserts */
 	indexInfo->ii_ReadyForInserts = !stmt->concurrent;
@@ -1513,7 +1514,7 @@ CheckPredicate(Expr *predicate)
 
 /*
  * Compute per-index-column information, including indexed column numbers
- * or index expressions, opclasses, and indoptions. Note, all output vectors
+ * or index expressions, opclasses and their options. Note, all output vectors
  * should be allocated for all columns, including "including" ones.
  */
 static void
@@ -1814,6 +1815,20 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 								accessMethodName)));
 		}
 
+		/* Set up the per-column opclass options (attoptions field). */
+		if (attribute->opclassopts)
+		{
+			Assert(attn < nkeycols);
+
+			if (!indexInfo->ii_OpclassOptions)
+				indexInfo->ii_OpclassOptions =
+					palloc0(sizeof(Datum) * indexInfo->ii_NumIndexAttrs);
+
+			indexInfo->ii_OpclassOptions[attn] =
+				transformRelOptions((Datum) 0, attribute->opclassopts,
+									NULL, NULL, false, false);
+		}
+
 		attn++;
 	}
 }
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 98519ef836..a7c8fa38a0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -5688,7 +5688,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	ReleaseSysCache(typeTuple);
 
-	InsertPgAttributeTuple(attrdesc, &attribute, NULL);
+	InsertPgAttributeTuple(attrdesc, &attribute, (Datum) 0, NULL);
 
 	table_close(attrdesc, RowExclusiveLock);
 
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 78deade89b..f5fad43eba 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2864,6 +2864,7 @@ _copyIndexElem(const IndexElem *from)
 	COPY_STRING_FIELD(indexcolname);
 	COPY_NODE_FIELD(collation);
 	COPY_NODE_FIELD(opclass);
+	COPY_NODE_FIELD(opclassopts);
 	COPY_SCALAR_FIELD(ordering);
 	COPY_SCALAR_FIELD(nulls_ordering);
 
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 4f2ebe5118..6b14095c82 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2546,6 +2546,7 @@ _equalIndexElem(const IndexElem *a, const IndexElem *b)
 	COMPARE_STRING_FIELD(indexcolname);
 	COMPARE_NODE_FIELD(collation);
 	COMPARE_NODE_FIELD(opclass);
+	COMPARE_NODE_FIELD(opclassopts);
 	COMPARE_SCALAR_FIELD(ordering);
 	COMPARE_SCALAR_FIELD(nulls_ordering);
 
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 237598e110..35806c0681 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2844,6 +2844,7 @@ _outIndexElem(StringInfo str, const IndexElem *node)
 	WRITE_STRING_FIELD(indexcolname);
 	WRITE_NODE_FIELD(collation);
 	WRITE_NODE_FIELD(opclass);
+	WRITE_NODE_FIELD(opclassopts);
 	WRITE_ENUM_FIELD(ordering, SortByDir);
 	WRITE_ENUM_FIELD(nulls_ordering, SortByNulls);
 }
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 2405acbf6f..5c6be745c0 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -362,6 +362,10 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 				info->nulls_first = NULL;
 			}
 
+			/* Fetch index opclass options */
+			info->opclassoptions =
+				RelationGetParsedOpclassOptions(indexRelation);
+
 			/*
 			 * Fetch the index expressions and predicate, if any.  We must
 			 * modify the copies we obtain from the relcache to have the
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 8311b1dd46..2feade5327 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -491,7 +491,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <alias>	alias_clause opt_alias_clause
 %type <list>	func_alias_clause
 %type <sortby>	sortby
-%type <ielem>	index_elem
+%type <ielem>	index_elem index_elem_options
 %type <node>	table_ref
 %type <jexpr>	joined_table
 %type <range>	relation_expr
@@ -7420,43 +7420,65 @@ index_params:	index_elem							{ $$ = list_make1($1); }
 			| index_params ',' index_elem			{ $$ = lappend($1, $3); }
 		;
 
+
+index_elem_options:
+	opt_collate opt_class opt_asc_desc opt_nulls_order
+		{
+			$$ = makeNode(IndexElem);
+			$$->name = NULL;
+			$$->expr = NULL;
+			$$->indexcolname = NULL;
+			$$->collation = $1;
+			$$->opclass = $2;
+			$$->opclassopts = NIL;
+			$$->ordering = $3;
+			$$->nulls_ordering = $4;
+		}
+	| opt_collate any_name reloptions opt_asc_desc opt_nulls_order
+		{
+			$$ = makeNode(IndexElem);
+			$$->name = NULL;
+			$$->expr = NULL;
+			$$->indexcolname = NULL;
+			$$->collation = $1;
+			$$->opclass = $2;
+			$$->opclassopts = $3;
+			$$->ordering = $4;
+			$$->nulls_ordering = $5;
+		}
+	| opt_collate DEFAULT reloptions opt_asc_desc opt_nulls_order
+		{
+			$$ = makeNode(IndexElem);
+			$$->name = NULL;
+			$$->expr = NULL;
+			$$->indexcolname = NULL;
+			$$->collation = $1;
+			$$->opclass = NIL;
+			$$->opclassopts = $3;
+			$$->ordering = $4;
+			$$->nulls_ordering = $5;
+		}
+	;
+
 /*
  * Index attributes can be either simple column references, or arbitrary
  * expressions in parens.  For backwards-compatibility reasons, we allow
  * an expression that's just a function call to be written without parens.
  */
-index_elem:	ColId opt_collate opt_class opt_asc_desc opt_nulls_order
+index_elem: ColId index_elem_options
 				{
-					$$ = makeNode(IndexElem);
+					$$ = $2;
 					$$->name = $1;
-					$$->expr = NULL;
-					$$->indexcolname = NULL;
-					$$->collation = $2;
-					$$->opclass = $3;
-					$$->ordering = $4;
-					$$->nulls_ordering = $5;
 				}
-			| func_expr_windowless opt_collate opt_class opt_asc_desc opt_nulls_order
+			| func_expr_windowless index_elem_options
 				{
-					$$ = makeNode(IndexElem);
-					$$->name = NULL;
+					$$ = $2;
 					$$->expr = $1;
-					$$->indexcolname = NULL;
-					$$->collation = $2;
-					$$->opclass = $3;
-					$$->ordering = $4;
-					$$->nulls_ordering = $5;
 				}
-			| '(' a_expr ')' opt_collate opt_class opt_asc_desc opt_nulls_order
+			| '(' a_expr ')' index_elem_options
 				{
-					$$ = makeNode(IndexElem);
-					$$->name = NULL;
+					$$ = $4;
 					$$->expr = $2;
-					$$->indexcolname = NULL;
-					$$->collation = $4;
-					$$->opclass = $5;
-					$$->ordering = $6;
-					$$->nulls_ordering = $7;
 				}
 		;
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 9dda4820af..ea49bef807 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -453,8 +453,6 @@ static void get_from_clause_coldeflist(RangeTblFunction *rtfunc,
 									   deparse_context *context);
 static void get_tablesample_def(TableSampleClause *tablesample,
 								deparse_context *context);
-static void get_opclass_name(Oid opclass, Oid actual_datatype,
-							 StringInfo buf);
 static Node *processIndirection(Node *node, deparse_context *context);
 static void printSubscripts(SubscriptingRef *sbsref, deparse_context *context);
 static char *get_relation_name(Oid relid);
@@ -469,6 +467,7 @@ static void add_cast_to(StringInfo buf, Oid typid);
 static char *generate_qualified_type_name(Oid typid);
 static text *string_to_text(char *str);
 static char *flatten_reloptions(Oid relid);
+static void get_reloptions(StringInfo buf, Datum reloptions);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -1198,6 +1197,7 @@ pg_get_indexdef_worker(Oid indexrelid, int colno,
 	oidvector  *indcollation;
 	oidvector  *indclass;
 	int2vector *indoption;
+	Datum	   *opcoptions = NULL;
 	StringInfoData buf;
 	char	   *str;
 	char	   *sep;
@@ -1233,6 +1233,9 @@ pg_get_indexdef_worker(Oid indexrelid, int colno,
 	Assert(!isnull);
 	indoption = (int2vector *) DatumGetPointer(indoptionDatum);
 
+	if (!attrsOnly)
+		opcoptions = RelationGetRawOpclassOptions(indexrelid, idxrec->indnatts);
+
 	/*
 	 * Fetch the pg_class tuple of the index relation
 	 */
@@ -1369,16 +1372,28 @@ pg_get_indexdef_worker(Oid indexrelid, int colno,
 		if (!attrsOnly && keyno < idxrec->indnkeyatts &&
 			(!colno || colno == keyno + 1))
 		{
+			bool		has_options = false;
 			int16		opt = indoption->values[keyno];
 			Oid			indcoll = indcollation->values[keyno];
 
+			if (opcoptions)
+				has_options = (opcoptions[keyno] != (Datum) 0);
+
 			/* Add collation, if not default for column */
 			if (OidIsValid(indcoll) && indcoll != keycolcollation)
 				appendStringInfo(&buf, " COLLATE %s",
 								 generate_collation_name((indcoll)));
 
 			/* Add the operator class name, if not default */
-			get_opclass_name(indclass->values[keyno], keycoltype, &buf);
+			get_opclass_name(indclass->values[keyno],
+							 has_options ? InvalidOid : keycoltype, &buf);
+
+			if (has_options)
+			{
+				appendStringInfoString(&buf, " (");
+				get_reloptions(&buf, opcoptions[keyno]);
+				appendStringInfoChar(&buf, ')');
+			}
 
 			/* Add options if relevant */
 			if (amroutine->amcanorder)
@@ -10459,7 +10474,7 @@ get_tablesample_def(TableSampleClause *tablesample, deparse_context *context)
  * actual_datatype.  (If you don't want this behavior, just pass
  * InvalidOid for actual_datatype.)
  */
-static void
+void
 get_opclass_name(Oid opclass, Oid actual_datatype,
 				 StringInfo buf)
 {
@@ -11168,6 +11183,62 @@ string_to_text(char *str)
 	return result;
 }
 
+/*
+ * Generate a C string representing a relation options from text[] datum.
+ */
+static void
+get_reloptions(StringInfo buf, Datum reloptions)
+{
+	Datum	   *options;
+	int			noptions;
+	int			i;
+
+	deconstruct_array(DatumGetArrayTypeP(reloptions),
+					  TEXTOID, -1, false, 'i',
+					  &options, NULL, &noptions);
+
+	for (i = 0; i < noptions; i++)
+	{
+		char	   *option = TextDatumGetCString(options[i]);
+		char	   *name;
+		char	   *separator;
+		char	   *value;
+
+		/*
+		 * Each array element should have the form name=value.  If the "="
+		 * is missing for some reason, treat it like an empty value.
+		 */
+		name = option;
+		separator = strchr(option, '=');
+		if (separator)
+		{
+			*separator = '\0';
+			value = separator + 1;
+		}
+		else
+			value = "";
+
+		if (i > 0)
+			appendStringInfoString(buf, ", ");
+		appendStringInfo(buf, "%s=", quote_identifier(name));
+
+		/*
+		 * In general we need to quote the value; but to avoid unnecessary
+		 * clutter, do not quote if it is an identifier that would not
+		 * need quoting.  (We could also allow numbers, but that is a bit
+		 * trickier than it looks --- for example, are leading zeroes
+		 * significant?  We don't want to assume very much here about what
+		 * custom reloptions might mean.)
+		 */
+		if (quote_identifier(value) == value)
+			appendStringInfoString(buf, value);
+		else
+			simple_quote_literal(buf, value);
+
+		pfree(option);
+	}
+}
+
 /*
  * Generate a C string representing a relation's reloptions, or NULL if none.
  */
@@ -11188,56 +11259,9 @@ flatten_reloptions(Oid relid)
 	if (!isnull)
 	{
 		StringInfoData buf;
-		Datum	   *options;
-		int			noptions;
-		int			i;
 
 		initStringInfo(&buf);
-
-		deconstruct_array(DatumGetArrayTypeP(reloptions),
-						  TEXTOID, -1, false, 'i',
-						  &options, NULL, &noptions);
-
-		for (i = 0; i < noptions; i++)
-		{
-			char	   *option = TextDatumGetCString(options[i]);
-			char	   *name;
-			char	   *separator;
-			char	   *value;
-
-			/*
-			 * Each array element should have the form name=value.  If the "="
-			 * is missing for some reason, treat it like an empty value.
-			 */
-			name = option;
-			separator = strchr(option, '=');
-			if (separator)
-			{
-				*separator = '\0';
-				value = separator + 1;
-			}
-			else
-				value = "";
-
-			if (i > 0)
-				appendStringInfoString(&buf, ", ");
-			appendStringInfo(&buf, "%s=", quote_identifier(name));
-
-			/*
-			 * In general we need to quote the value; but to avoid unnecessary
-			 * clutter, do not quote if it is an identifier that would not
-			 * need quoting.  (We could also allow numbers, but that is a bit
-			 * trickier than it looks --- for example, are leading zeroes
-			 * significant?  We don't want to assume very much here about what
-			 * custom reloptions might mean.)
-			 */
-			if (quote_identifier(value) == value)
-				appendStringInfoString(&buf, value);
-			else
-				simple_quote_literal(&buf, value);
-
-			pfree(option);
-		}
+		get_reloptions(&buf, reloptions);
 
 		result = buf.data;
 	}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2b992d7832..62caa4cfaf 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5173,6 +5173,105 @@ GetRelationPublicationActions(Relation relation)
 	return pubactions;
 }
 
+/*
+ * RelationGetIndexOpclassOptions -- get opclass-specific options for the index
+ */
+Datum *
+RelationGetRawOpclassOptions(Oid indexrelid, int16 natts)
+{
+	Datum	   *options = NULL;
+	int16		attnum;
+
+	for (attnum = 1; attnum <= natts; attnum++)
+	{
+		HeapTuple	tuple;
+		Datum		attopts;
+		bool		isnull;
+
+		tuple = SearchSysCache2(ATTNUM, ObjectIdGetDatum(indexrelid),
+								Int16GetDatum(attnum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+				 attnum, indexrelid);
+
+		attopts = SysCacheGetAttr(ATTNAME, tuple, Anum_pg_attribute_attoptions,
+								  &isnull);
+
+		if (!isnull)
+		{
+			if (!options)
+				options = palloc0(sizeof(Datum) * natts);
+
+			options[attnum - 1] = datumCopy(attopts, false, -1);	/* text */
+		}
+
+		ReleaseSysCache(tuple);
+	}
+
+	return options;
+}
+
+/*
+ * RelationGetOpclassOptions -- get parsed opclass-specific options for an index
+ */
+bytea **
+RelationGetParsedOpclassOptions(Relation relation)
+{
+	MemoryContext oldcxt;
+	bytea	  **opts;
+	Datum	   *rawopts;
+	int			natts = RelationGetNumberOfAttributes(relation);
+	int			i;
+
+	/* Try to copy cached options. */
+	if (relation->rd_opcoptions)
+	{
+		opts = palloc(sizeof(*opts) * natts);
+
+		for (i = 0; i < natts; i++)
+		{
+			bytea	   *opt = relation->rd_opcoptions[i];
+
+			opts[i] = !opt ? NULL : (bytea *)
+				DatumGetPointer(datumCopy(PointerGetDatum(opt), false, -1));
+		}
+
+		return opts;
+	}
+
+	/* Get and parse opclass options. */
+	opts = palloc0(sizeof(*opts) * natts);
+
+	rawopts = RelationGetRawOpclassOptions(RelationGetRelid(relation), natts);
+
+	for (i = 0; i < natts; i++)
+	{
+		Datum		options = rawopts ? rawopts[i] : (Datum) 0;
+
+		opts[i] = index_opclass_options(relation, i + 1, options, false);
+
+		if (options != (Datum) 0)
+			pfree(DatumGetPointer(options));
+	}
+
+	if (rawopts)
+		pfree(rawopts);
+
+	/* Copy parsed options to the cache. */
+	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+
+	relation->rd_opcoptions = palloc(sizeof(*opts) * natts);
+
+	for (i = 0; i < natts; i++)
+		relation->rd_opcoptions[i] = !opts[i] ? NULL : (bytea *)
+			DatumGetPointer(datumCopy(PointerGetDatum(opts[i]), false, -1));
+
+	MemoryContextSwitchTo(oldcxt);
+
+	return opts;
+}
+
 /*
  * Routines to support ereport() reports of relation-related errors
  *
diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index 6e3db06eed..9c06d1a094 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -103,6 +103,12 @@ typedef void (*amcostestimate_function) (struct PlannerInfo *root,
 typedef bytea *(*amoptions_function) (Datum reloptions,
 									  bool validate);
 
+/* parse column opclass-specific options */
+typedef bytea *(*amopclassoptions_function) (Relation index,
+											 AttrNumber colno,
+											 Datum attoptions,
+											 bool validate);
+
 /* report AM, index, or index column property */
 typedef bool (*amproperty_function) (Oid index_oid, int attno,
 									 IndexAMProperty prop, const char *propname,
@@ -215,6 +221,7 @@ typedef struct IndexAmRoutine
 	amcanreturn_function amcanreturn;	/* can be NULL */
 	amcostestimate_function amcostestimate;
 	amoptions_function amoptions;
+	amopclassoptions_function amopclassoptions;
 	amproperty_function amproperty; /* can be NULL */
 	ambuildphasename_function ambuildphasename; /* can be NULL */
 	amvalidate_function amvalidate;
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index 8c053be2ca..aa170f6de6 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -180,6 +180,11 @@ extern FmgrInfo *index_getprocinfo(Relation irel, AttrNumber attnum,
 extern void index_store_float8_orderby_distances(IndexScanDesc scan,
 												 Oid *orderByTypes, double *distances,
 												 bool recheckOrderBy);
+extern bytea *index_opclass_options(Relation relation, AttrNumber attnum,
+					  Datum attoptions, bool validate);
+extern bytea *index_opclass_options_generic(Relation relation,
+							  AttrNumber attnum, uint16 procnum,
+							  Datum attoptions, bool validate);
 
 /*
  * index access method support routines (in genam.c)
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index a1912f41e6..8dea2d8e69 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -263,12 +263,17 @@ extern bytea *extractRelOptions(HeapTuple tuple, TupleDesc tupdesc,
 								amoptions_function amoptions);
 extern relopt_value *parseRelOptions(Datum options, bool validate,
 									 relopt_kind kind, int *numrelopts);
+extern relopt_value *parseLocalRelOptions(Datum options, bool validate,
+					 relopt_gen **gen, int nelems);
 extern void *allocateReloptStruct(Size base, relopt_value *options,
 								  int numoptions);
 extern void fillRelOptions(void *rdopts, Size basesize,
 						   relopt_value *options, int numoptions,
 						   bool validate,
 						   const relopt_parse_elt *elems, int nelems);
+extern void *parseAndFillLocalRelOptions(Datum options, relopt_gen *optgen[],
+							int offsets[], int noptions, size_t base_size,
+							bool validate);
 
 extern bytea *default_reloptions(Datum reloptions, bool validate,
 								 relopt_kind kind);
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index eec71c29d5..866e94d04f 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -94,6 +94,7 @@ extern List *heap_truncate_find_FKs(List *relationIds);
 
 extern void InsertPgAttributeTuple(Relation pg_attribute_rel,
 								   Form_pg_attribute new_attribute,
+								   Datum attoptions,
 								   CatalogIndexState indstate);
 
 extern void InsertPgClassTuple(Relation pg_class_desc,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 99b9fa414f..e0d6c6c375 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -138,6 +138,7 @@ typedef struct ExprState
  *		UniqueProcs
  *		UniqueStrats
  *		Unique				is it a unique index?
+ *		OpclassOptions		opclass-specific options, or NULL if none
  *		ReadyForInserts		is it valid for inserts?
  *		Concurrent			are we doing a concurrent index build?
  *		BrokenHotChain		did we detect any broken HOT chains?
@@ -166,6 +167,7 @@ typedef struct IndexInfo
 	Oid		   *ii_UniqueOps;	/* array with one entry per column */
 	Oid		   *ii_UniqueProcs; /* array with one entry per column */
 	uint16	   *ii_UniqueStrats;	/* array with one entry per column */
+	Datum	   *ii_OpclassOptions;	/* array with one entry per column */
 	bool		ii_Unique;
 	bool		ii_ReadyForInserts;
 	bool		ii_Concurrent;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 2a8edf934f..15699971a7 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -701,6 +701,7 @@ typedef struct IndexElem
 	char	   *indexcolname;	/* name for index column; NULL = default */
 	List	   *collation;		/* name of collation; NIL = default */
 	List	   *opclass;		/* name of desired opclass; NIL = default */
+	List	   *opclassopts;	/* opclass-specific options, or NIL */
 	SortByDir	ordering;		/* ASC/DESC/default */
 	SortByNulls nulls_ordering; /* FIRST/LAST/default */
 } IndexElem;
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 4b7703d478..f8a935f501 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -802,6 +802,7 @@ struct IndexOptInfo
 	Oid		   *sortopfamily;	/* OIDs of btree opfamilies, if orderable */
 	bool	   *reverse_sort;	/* is sort order descending? */
 	bool	   *nulls_first;	/* do NULLs come first in the sort order? */
+	bytea	 **opclassoptions;	/* opclass-specific options for columns */
 	bool	   *canreturn;		/* which index cols can be returned in an
 								 * index-only scan? */
 	Oid			relam;			/* OID of the access method (in pg_am) */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index d7f33abce3..8440e225ee 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -175,6 +175,7 @@ typedef struct RelationData
 	uint16	   *rd_exclstrats;	/* exclusion ops' strategy numbers, if any */
 	void	   *rd_amcache;		/* available for use by index AM */
 	Oid		   *rd_indcollation;	/* OIDs of index collations */
+	bytea	  **rd_opcoptions;	/* parsed opclass-specific options */
 
 	/*
 	 * foreign-table support
diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h
index d9c10ffcba..562929c058 100644
--- a/src/include/utils/relcache.h
+++ b/src/include/utils/relcache.h
@@ -14,6 +14,7 @@
 #ifndef RELCACHE_H
 #define RELCACHE_H
 
+#include "postgres.h"
 #include "access/tupdesc.h"
 #include "nodes/bitmapset.h"
 
@@ -49,6 +50,8 @@ extern Oid	RelationGetPrimaryKeyIndex(Relation relation);
 extern Oid	RelationGetReplicaIndex(Relation relation);
 extern List *RelationGetIndexExpressions(Relation relation);
 extern List *RelationGetIndexPredicate(Relation relation);
+extern bytea **RelationGetParsedOpclassOptions(Relation relation);
+extern Datum *RelationGetRawOpclassOptions(Oid indexrelid, int16 natts);
 
 typedef enum IndexAttrBitmapKind
 {
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index d34cad2f4b..68d7ca892e 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -35,5 +35,7 @@ extern List *select_rtable_names_for_explain(List *rtable,
 											 Bitmapset *rels_used);
 extern char *generate_collation_name(Oid collid);
 extern char *get_range_partbound_string(List *bound_datums);
+extern void get_opclass_name(Oid opclass, Oid actual_datatype, StringInfo buf);
+
 
 #endif							/* RULEUTILS_H */
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index acab8e0b11..33f2bf0e3f 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -244,6 +244,11 @@ select reloptions from pg_class WHERE oid = 'btree_idx1'::regclass;
  {vacuum_cleanup_index_scale_factor=70.0}
 (1 row)
 
+-- Test unsupported btree opclass parameters
+create index on btree_tall_tbl (id int4_ops(foo=1));
+ERROR:  access method "btree" does not support opclass options 
+create index on btree_tall_tbl (id default(foo=1));
+ERROR:  access method "btree" does not support opclass options 
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 48eaf4fe42..aecd690a01 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -121,6 +121,10 @@ create index btree_idx_err on btree_test(a) with (vacuum_cleanup_index_scale_fac
 alter index btree_idx1 set (vacuum_cleanup_index_scale_factor = 70.0);
 select reloptions from pg_class WHERE oid = 'btree_idx1'::regclass;
 
+-- Test unsupported btree opclass parameters
+create index on btree_tall_tbl (id int4_ops(foo=1));
+create index on btree_tall_tbl (id default(foo=1));
+
 --
 -- Test for multilevel page deletion
 --
-- 
2.20.1

0002-Add-opclass-parameters-to-GiST-20190611.patchtext/plain; charset=us-asciiDownload
From ddd99a8fb48774cfaf9ca151ebb8a5238bc1d2b3 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 21:17:42 +0200
Subject: [PATCH 02/10] Add opclass parameters to GiST

---
 doc/src/sgml/xindex.sgml               |  5 +++
 src/backend/access/gist/gist.c         |  2 ++
 src/backend/access/gist/gistget.c      | 12 ++++---
 src/backend/access/gist/gistsplit.c    | 15 +++++----
 src/backend/access/gist/gistutil.c     | 44 ++++++++++++++++++--------
 src/backend/access/gist/gistvalidate.c | 34 +++++++++++++-------
 src/include/access/gist.h              |  3 +-
 src/include/access/gist_private.h      |  4 +++
 8 files changed, 82 insertions(+), 37 deletions(-)

diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 9446f8b836..8c5b5289d7 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -546,6 +546,11 @@
        index-only scans (optional)</entry>
        <entry>9</entry>
       </row>
+      <row>
+       <entry><function>options</function></entry>
+       <entry>parse opclass-specific options (optional)</entry>
+       <entry>10</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index 470b121e7d..8e3460f456 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -98,6 +98,7 @@ gisthandler(PG_FUNCTION_ARGS)
 	amroutine->amestimateparallelscan = NULL;
 	amroutine->aminitparallelscan = NULL;
 	amroutine->amparallelrescan = NULL;
+	amroutine->amopclassoptions = gistopclassoptions;
 
 	PG_RETURN_POINTER(amroutine);
 }
@@ -1528,6 +1529,7 @@ initGISTstate(Relation index)
 	giststate->scanCxt = scanCxt;
 	giststate->tempCxt = scanCxt;	/* caller must change this if needed */
 	giststate->leafTupdesc = index->rd_att;
+	giststate->opclassoptions = RelationGetParsedOpclassOptions(index);
 
 	/*
 	 * The truncated tupdesc for non-leaf index tuples, which doesn't contain
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index 8108fbb7d8..abfe659ca5 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -196,6 +196,7 @@ gistindex_keytest(IndexScanDesc scan,
 			Datum		test;
 			bool		recheck;
 			GISTENTRY	de;
+			bytea	   *options = giststate->opclassoptions[key->sk_attno - 1];
 
 			gistdentryinit(giststate, key->sk_attno - 1, &de,
 						   datum, r, page, offset,
@@ -216,13 +217,14 @@ gistindex_keytest(IndexScanDesc scan,
 			 */
 			recheck = true;
 
-			test = FunctionCall5Coll(&key->sk_func,
+			test = FunctionCall6Coll(&key->sk_func,
 									 key->sk_collation,
 									 PointerGetDatum(&de),
 									 key->sk_argument,
 									 Int16GetDatum(key->sk_strategy),
 									 ObjectIdGetDatum(key->sk_subtype),
-									 PointerGetDatum(&recheck));
+									 PointerGetDatum(&recheck),
+									 PointerGetDatum(options));
 
 			if (!DatumGetBool(test))
 				return false;
@@ -257,6 +259,7 @@ gistindex_keytest(IndexScanDesc scan,
 			Datum		dist;
 			bool		recheck;
 			GISTENTRY	de;
+			bytea	   *options = giststate->opclassoptions[key->sk_attno - 1];
 
 			gistdentryinit(giststate, key->sk_attno - 1, &de,
 						   datum, r, page, offset,
@@ -279,13 +282,14 @@ gistindex_keytest(IndexScanDesc scan,
 			 * about the flag, but are expected to never be lossy.
 			 */
 			recheck = false;
-			dist = FunctionCall5Coll(&key->sk_func,
+			dist = FunctionCall6Coll(&key->sk_func,
 									 key->sk_collation,
 									 PointerGetDatum(&de),
 									 key->sk_argument,
 									 Int16GetDatum(key->sk_strategy),
 									 ObjectIdGetDatum(key->sk_subtype),
-									 PointerGetDatum(&recheck));
+									 PointerGetDatum(&recheck),
+									 PointerGetDatum(options));
 			*recheck_distances_p |= recheck;
 			*distance_p = DatumGetFloat8(dist);
 		}
diff --git a/src/backend/access/gist/gistsplit.c b/src/backend/access/gist/gistsplit.c
index 6a9c54d86c..2b3cb967e1 100644
--- a/src/backend/access/gist/gistsplit.c
+++ b/src/backend/access/gist/gistsplit.c
@@ -378,18 +378,20 @@ genericPickSplit(GISTSTATE *giststate, GistEntryVector *entryvec, GIST_SPLITVEC
 	evec->n = v->spl_nleft;
 	memcpy(evec->vector, entryvec->vector + FirstOffsetNumber,
 		   sizeof(GISTENTRY) * evec->n);
-	v->spl_ldatum = FunctionCall2Coll(&giststate->unionFn[attno],
+	v->spl_ldatum = FunctionCall3Coll(&giststate->unionFn[attno],
 									  giststate->supportCollation[attno],
 									  PointerGetDatum(evec),
-									  PointerGetDatum(&nbytes));
+									  PointerGetDatum(&nbytes),
+									  PointerGetDatum(giststate->opclassoptions[attno]));
 
 	evec->n = v->spl_nright;
 	memcpy(evec->vector, entryvec->vector + FirstOffsetNumber + v->spl_nleft,
 		   sizeof(GISTENTRY) * evec->n);
-	v->spl_rdatum = FunctionCall2Coll(&giststate->unionFn[attno],
+	v->spl_rdatum = FunctionCall3Coll(&giststate->unionFn[attno],
 									  giststate->supportCollation[attno],
 									  PointerGetDatum(evec),
-									  PointerGetDatum(&nbytes));
+									  PointerGetDatum(&nbytes),
+									  PointerGetDatum(giststate->opclassoptions[attno]));
 }
 
 /*
@@ -430,10 +432,11 @@ gistUserPicksplit(Relation r, GistEntryVector *entryvec, int attno, GistSplitVec
 	 * Let the opclass-specific PickSplit method do its thing.  Note that at
 	 * this point we know there are no null keys in the entryvec.
 	 */
-	FunctionCall2Coll(&giststate->picksplitFn[attno],
+	FunctionCall3Coll(&giststate->picksplitFn[attno],
 					  giststate->supportCollation[attno],
 					  PointerGetDatum(entryvec),
-					  PointerGetDatum(sv));
+					  PointerGetDatum(sv),
+					  PointerGetDatum(giststate->opclassoptions[attno]));
 
 	if (sv->spl_nleft == 0 || sv->spl_nright == 0)
 	{
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index 49df05653b..000f10103c 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -201,10 +201,11 @@ gistMakeUnionItVec(GISTSTATE *giststate, IndexTuple *itvec, int len,
 			}
 
 			/* Make union and store in attr array */
-			attr[i] = FunctionCall2Coll(&giststate->unionFn[i],
+			attr[i] = FunctionCall3Coll(&giststate->unionFn[i],
 										giststate->supportCollation[i],
 										PointerGetDatum(evec),
-										PointerGetDatum(&attrsize));
+										PointerGetDatum(&attrsize),
+										PointerGetDatum(giststate->opclassoptions[i]));
 
 			isnull[i] = false;
 		}
@@ -270,10 +271,11 @@ gistMakeUnionKey(GISTSTATE *giststate, int attno,
 		}
 
 		*dstisnull = false;
-		*dst = FunctionCall2Coll(&giststate->unionFn[attno],
+		*dst = FunctionCall3Coll(&giststate->unionFn[attno],
 								 giststate->supportCollation[attno],
 								 PointerGetDatum(evec),
-								 PointerGetDatum(&dstsize));
+								 PointerGetDatum(&dstsize),
+								 PointerGetDatum(giststate->opclassoptions[attno]));
 	}
 }
 
@@ -282,10 +284,11 @@ gistKeyIsEQ(GISTSTATE *giststate, int attno, Datum a, Datum b)
 {
 	bool		result;
 
-	FunctionCall3Coll(&giststate->equalFn[attno],
+	FunctionCall4Coll(&giststate->equalFn[attno],
 					  giststate->supportCollation[attno],
 					  a, b,
-					  PointerGetDatum(&result));
+					  PointerGetDatum(&result),
+					  PointerGetDatum(giststate->opclassoptions[attno]));
 	return result;
 }
 
@@ -559,9 +562,10 @@ gistdentryinit(GISTSTATE *giststate, int nkey, GISTENTRY *e,
 			return;
 
 		dep = (GISTENTRY *)
-			DatumGetPointer(FunctionCall1Coll(&giststate->decompressFn[nkey],
+			DatumGetPointer(FunctionCall2Coll(&giststate->decompressFn[nkey],
 											  giststate->supportCollation[nkey],
-											  PointerGetDatum(e)));
+											  PointerGetDatum(e),
+											  PointerGetDatum(giststate->opclassoptions[nkey])));
 		/* decompressFn may just return the given pointer */
 		if (dep != e)
 			gistentryinit(*e, dep->key, dep->rel, dep->page, dep->offset,
@@ -596,9 +600,10 @@ gistFormTuple(GISTSTATE *giststate, Relation r,
 			/* there may not be a compress function in opclass */
 			if (OidIsValid(giststate->compressFn[i].fn_oid))
 				cep = (GISTENTRY *)
-					DatumGetPointer(FunctionCall1Coll(&giststate->compressFn[i],
+					DatumGetPointer(FunctionCall2Coll(&giststate->compressFn[i],
 													  giststate->supportCollation[i],
-													  PointerGetDatum(&centry)));
+													  PointerGetDatum(&centry),
+													  PointerGetDatum(giststate->opclassoptions[i])));
 			else
 				cep = &centry;
 			compatt[i] = cep->key;
@@ -643,9 +648,10 @@ gistFetchAtt(GISTSTATE *giststate, int nkey, Datum k, Relation r)
 	gistentryinit(fentry, k, r, NULL, (OffsetNumber) 0, false);
 
 	fep = (GISTENTRY *)
-		DatumGetPointer(FunctionCall1Coll(&giststate->fetchFn[nkey],
+		DatumGetPointer(FunctionCall2Coll(&giststate->fetchFn[nkey],
 										  giststate->supportCollation[nkey],
-										  PointerGetDatum(&fentry)));
+										  PointerGetDatum(&fentry),
+										  PointerGetDatum(giststate->opclassoptions[nkey])));
 
 	/* fetchFn set 'key', return it to the caller */
 	return fep->key;
@@ -722,11 +728,12 @@ gistpenalty(GISTSTATE *giststate, int attno,
 	if (giststate->penaltyFn[attno].fn_strict == false ||
 		(isNullOrig == false && isNullAdd == false))
 	{
-		FunctionCall3Coll(&giststate->penaltyFn[attno],
+		FunctionCall4Coll(&giststate->penaltyFn[attno],
 						  giststate->supportCollation[attno],
 						  PointerGetDatum(orig),
 						  PointerGetDatum(add),
-						  PointerGetDatum(&penalty));
+						  PointerGetDatum(&penalty),
+						  PointerGetDatum(giststate->opclassoptions[attno]));
 		/* disallow negative or NaN penalty */
 		if (isnan(penalty) || penalty < 0.0)
 			penalty = 0.0;
@@ -915,6 +922,15 @@ gistoptions(Datum reloptions, bool validate)
 	return (bytea *) rdopts;
 }
 
+bytea *
+gistopclassoptions(Relation index, AttrNumber attnum, Datum attoptions,
+				   bool validate)
+{
+	return index_opclass_options_generic(index, attnum, GIST_OPCLASSOPT_PROC,
+										 attoptions, validate);
+}
+
+
 /*
  *	gistproperty() -- Check boolean properties of indexes.
  *
diff --git a/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index dfc1a87a75..7ae820070b 100644
--- a/src/backend/access/gist/gistvalidate.c
+++ b/src/backend/access/gist/gistvalidate.c
@@ -108,37 +108,46 @@ gistvalidate(Oid opclassoid)
 		{
 			case GIST_CONSISTENT_PROC:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, false,
-											5, 5, INTERNALOID, opcintype,
-											INT2OID, OIDOID, INTERNALOID);
+											5, 6, INTERNALOID, opcintype,
+											INT2OID, OIDOID, INTERNALOID,
+											INTERNALOID);
 				break;
 			case GIST_UNION_PROC:
 				ok = check_amproc_signature(procform->amproc, opckeytype, false,
-											2, 2, INTERNALOID, INTERNALOID);
+											2, 3, INTERNALOID, INTERNALOID,
+											INTERNALOID);
 				break;
 			case GIST_COMPRESS_PROC:
 			case GIST_DECOMPRESS_PROC:
 			case GIST_FETCH_PROC:
 				ok = check_amproc_signature(procform->amproc, INTERNALOID, true,
-											1, 1, INTERNALOID);
+											1, 2, INTERNALOID, INTERNALOID);
 				break;
 			case GIST_PENALTY_PROC:
 				ok = check_amproc_signature(procform->amproc, INTERNALOID, true,
-											3, 3, INTERNALOID,
-											INTERNALOID, INTERNALOID);
+											3, 4, INTERNALOID,
+											INTERNALOID, INTERNALOID,
+											INTERNALOID);
 				break;
 			case GIST_PICKSPLIT_PROC:
 				ok = check_amproc_signature(procform->amproc, INTERNALOID, true,
-											2, 2, INTERNALOID, INTERNALOID);
+											2, 3, INTERNALOID, INTERNALOID,
+											INTERNALOID);
 				break;
 			case GIST_EQUAL_PROC:
 				ok = check_amproc_signature(procform->amproc, INTERNALOID, false,
-											3, 3, opckeytype, opckeytype,
-											INTERNALOID);
+											3, 4, opckeytype, opckeytype,
+											INTERNALOID, INTERNALOID);
 				break;
 			case GIST_DISTANCE_PROC:
 				ok = check_amproc_signature(procform->amproc, FLOAT8OID, false,
-											5, 5, INTERNALOID, opcintype,
-											INT2OID, OIDOID, INTERNALOID);
+											5, 6, INTERNALOID, opcintype,
+											INT2OID, OIDOID, INTERNALOID,
+											INTERNALOID);
+				break;
+			case GIST_OPCLASSOPT_PROC:
+				ok = check_amproc_signature(procform->amproc, INTERNALOID, false,
+											2, 2, INTERNALOID, BOOLOID);
 				break;
 			default:
 				ereport(INFO,
@@ -259,7 +268,8 @@ gistvalidate(Oid opclassoid)
 			(opclassgroup->functionset & (((uint64) 1) << i)) != 0)
 			continue;			/* got it */
 		if (i == GIST_DISTANCE_PROC || i == GIST_FETCH_PROC ||
-			i == GIST_COMPRESS_PROC || i == GIST_DECOMPRESS_PROC)
+			i == GIST_COMPRESS_PROC || i == GIST_DECOMPRESS_PROC ||
+			i == GIST_OPCLASSOPT_PROC)
 			continue;			/* optional methods */
 		ereport(INFO,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/include/access/gist.h b/src/include/access/gist.h
index 6902f4115b..0c57a64369 100644
--- a/src/include/access/gist.h
+++ b/src/include/access/gist.h
@@ -34,7 +34,8 @@
 #define GIST_EQUAL_PROC					7
 #define GIST_DISTANCE_PROC				8
 #define GIST_FETCH_PROC					9
-#define GISTNProcs					9
+#define GIST_OPCLASSOPT_PROC			10
+#define GISTNProcs						10
 
 /*
  * Page opaque data in a GiST index page.
diff --git a/src/include/access/gist_private.h b/src/include/access/gist_private.h
index f80694bf9a..d46b6b89f9 100644
--- a/src/include/access/gist_private.h
+++ b/src/include/access/gist_private.h
@@ -96,6 +96,8 @@ typedef struct GISTSTATE
 
 	/* Collations to pass to the support functions */
 	Oid			supportCollation[INDEX_MAX_KEYS];
+
+	bytea	  **opclassoptions;	/* parsed opclass-specific options */
 } GISTSTATE;
 
 
@@ -462,6 +464,8 @@ extern bool gistvalidate(Oid opclassoid);
 #define GIST_DEFAULT_FILLFACTOR		90
 
 extern bytea *gistoptions(Datum reloptions, bool validate);
+extern bytea *gistopclassoptions(Relation index, AttrNumber colno,
+				   Datum options, bool validate);
 extern bool gistproperty(Oid index_oid, int attno,
 						 IndexAMProperty prop, const char *propname,
 						 bool *res, bool *isnull);
-- 
2.20.1

0003-Add-opclass-parameters-to-GIN-20190611.patchtext/plain; charset=us-asciiDownload
From f056cb2deddde2ff0cbef894be2699710720507b Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 21:18:18 +0200
Subject: [PATCH 03/10] Add opclass parameters to GIN

---
 doc/src/sgml/xindex.sgml             |  7 ++++++
 src/backend/access/gin/ginget.c      | 11 +++++-----
 src/backend/access/gin/ginlogic.c    | 15 +++++++------
 src/backend/access/gin/ginscan.c     |  6 ++++--
 src/backend/access/gin/ginutil.c     | 27 +++++++++++++++++------
 src/backend/access/gin/ginvalidate.c | 32 +++++++++++++++++-----------
 src/backend/utils/adt/selfuncs.c     |  6 ++++--
 src/include/access/gin.h             |  3 ++-
 src/include/access/gin_private.h     |  5 +++++
 9 files changed, 78 insertions(+), 34 deletions(-)

diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 8c5b5289d7..658ec9bc5a 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -665,6 +665,13 @@
        </entry>
        <entry>6</entry>
       </row>
+      <row>
+       <entry><function>options</function></entry>
+       <entry>
+        parse opclass-specific options (optional)
+       </entry>
+       <entry>7</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/access/gin/ginget.c b/src/backend/access/gin/ginget.c
index b18ae2b3ed..547b7b4762 100644
--- a/src/backend/access/gin/ginget.c
+++ b/src/backend/access/gin/ginget.c
@@ -188,13 +188,13 @@ collectMatchBitmap(GinBtreeData *btree, GinBtreeStack *stack,
 			 * case cmp < 0 => not match and continue scan
 			 *----------
 			 */
-			cmp = DatumGetInt32(FunctionCall4Coll(&btree->ginstate->comparePartialFn[attnum - 1],
+			cmp = DatumGetInt32(FunctionCall5Coll(&btree->ginstate->comparePartialFn[attnum - 1],
 												  btree->ginstate->supportCollation[attnum - 1],
 												  scanEntry->queryKey,
 												  idatum,
 												  UInt16GetDatum(scanEntry->strategy),
-												  PointerGetDatum(scanEntry->extra_data)));
-
+												  PointerGetDatum(scanEntry->extra_data),
+												  PointerGetDatum(btree->ginstate->opclassOptions[attnum - 1])));
 			if (cmp > 0)
 				return true;
 			else if (cmp < 0)
@@ -1508,12 +1508,13 @@ matchPartialInPendingList(GinState *ginstate, Page page,
 		 * case cmp < 0 => not match and continue scan
 		 *----------
 		 */
-		cmp = DatumGetInt32(FunctionCall4Coll(&ginstate->comparePartialFn[entry->attnum - 1],
+		cmp = DatumGetInt32(FunctionCall5Coll(&ginstate->comparePartialFn[entry->attnum - 1],
 											  ginstate->supportCollation[entry->attnum - 1],
 											  entry->queryKey,
 											  datum[off - 1],
 											  UInt16GetDatum(entry->strategy),
-											  PointerGetDatum(entry->extra_data)));
+											  PointerGetDatum(entry->extra_data),
+											  PointerGetDatum(ginstate->opclassOptions[entry->attnum - 1])));
 		if (cmp == 0)
 			return true;
 		else if (cmp > 0)
diff --git a/src/backend/access/gin/ginlogic.c b/src/backend/access/gin/ginlogic.c
index 8f85978972..028b29a8c0 100644
--- a/src/backend/access/gin/ginlogic.c
+++ b/src/backend/access/gin/ginlogic.c
@@ -76,7 +76,7 @@ directBoolConsistentFn(GinScanKey key)
 	 */
 	key->recheckCurItem = true;
 
-	return DatumGetBool(FunctionCall8Coll(key->consistentFmgrInfo,
+	return DatumGetBool(FunctionCall9Coll(key->consistentFmgrInfo,
 										  key->collation,
 										  PointerGetDatum(key->entryRes),
 										  UInt16GetDatum(key->strategy),
@@ -85,7 +85,8 @@ directBoolConsistentFn(GinScanKey key)
 										  PointerGetDatum(key->extra_data),
 										  PointerGetDatum(&key->recheckCurItem),
 										  PointerGetDatum(key->queryValues),
-										  PointerGetDatum(key->queryCategories)));
+										  PointerGetDatum(key->queryCategories),
+										  PointerGetDatum(key->opclassOptions)));
 }
 
 /*
@@ -94,7 +95,7 @@ directBoolConsistentFn(GinScanKey key)
 static GinTernaryValue
 directTriConsistentFn(GinScanKey key)
 {
-	return DatumGetGinTernaryValue(FunctionCall7Coll(
+	return DatumGetGinTernaryValue(FunctionCall8Coll(
 													 key->triConsistentFmgrInfo,
 													 key->collation,
 													 PointerGetDatum(key->entryRes),
@@ -103,7 +104,8 @@ directTriConsistentFn(GinScanKey key)
 													 UInt32GetDatum(key->nuserentries),
 													 PointerGetDatum(key->extra_data),
 													 PointerGetDatum(key->queryValues),
-													 PointerGetDatum(key->queryCategories)));
+													 PointerGetDatum(key->queryCategories),
+													 PointerGetDatum(key->opclassOptions)));
 }
 
 /*
@@ -116,7 +118,7 @@ shimBoolConsistentFn(GinScanKey key)
 {
 	GinTernaryValue result;
 
-	result = DatumGetGinTernaryValue(FunctionCall7Coll(
+	result = DatumGetGinTernaryValue(FunctionCall8Coll(
 													   key->triConsistentFmgrInfo,
 													   key->collation,
 													   PointerGetDatum(key->entryRes),
@@ -125,7 +127,8 @@ shimBoolConsistentFn(GinScanKey key)
 													   UInt32GetDatum(key->nuserentries),
 													   PointerGetDatum(key->extra_data),
 													   PointerGetDatum(key->queryValues),
-													   PointerGetDatum(key->queryCategories)));
+													   PointerGetDatum(key->queryCategories),
+													   PointerGetDatum(key->opclassOptions)));
 	if (result == GIN_MAYBE)
 	{
 		key->recheckCurItem = true;
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index 74d9821ac1..d1384a33d4 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -156,6 +156,7 @@ ginFillScanKey(GinScanOpaque so, OffsetNumber attnum,
 	key->strategy = strategy;
 	key->searchMode = searchMode;
 	key->attnum = attnum;
+	key->opclassOptions = ginstate->opclassOptions[attnum - 1];
 
 	ItemPointerSetMin(&key->curItem);
 	key->curItemMatches = false;
@@ -310,7 +311,7 @@ ginNewScanKey(IndexScanDesc scan)
 
 		/* OK to call the extractQueryFn */
 		queryValues = (Datum *)
-			DatumGetPointer(FunctionCall7Coll(&so->ginstate.extractQueryFn[skey->sk_attno - 1],
+			DatumGetPointer(FunctionCall8Coll(&so->ginstate.extractQueryFn[skey->sk_attno - 1],
 											  so->ginstate.supportCollation[skey->sk_attno - 1],
 											  skey->sk_argument,
 											  PointerGetDatum(&nQueryValues),
@@ -318,7 +319,8 @@ ginNewScanKey(IndexScanDesc scan)
 											  PointerGetDatum(&partial_matches),
 											  PointerGetDatum(&extra_data),
 											  PointerGetDatum(&nullFlags),
-											  PointerGetDatum(&searchMode)));
+											  PointerGetDatum(&searchMode),
+											  PointerGetDatum(so->ginstate.opclassOptions[skey->sk_attno - 1])));
 
 		/*
 		 * If bogus searchMode is returned, treat as GIN_SEARCH_MODE_ALL; note
diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c
index cf9699ad18..03874909b0 100644
--- a/src/backend/access/gin/ginutil.c
+++ b/src/backend/access/gin/ginutil.c
@@ -63,6 +63,7 @@ ginhandler(PG_FUNCTION_ARGS)
 	amroutine->amcanreturn = NULL;
 	amroutine->amcostestimate = gincostestimate;
 	amroutine->amoptions = ginoptions;
+	amroutine->amopclassoptions = ginopclassoptions;
 	amroutine->amproperty = NULL;
 	amroutine->ambuildphasename = NULL;
 	amroutine->amvalidate = ginvalidate;
@@ -96,6 +97,7 @@ initGinState(GinState *state, Relation index)
 	state->index = index;
 	state->oneCol = (origTupdesc->natts == 1) ? true : false;
 	state->origTupdesc = origTupdesc;
+	state->opclassOptions = RelationGetParsedOpclassOptions(index);
 
 	for (i = 0; i < origTupdesc->natts; i++)
 	{
@@ -399,9 +401,10 @@ ginCompareEntries(GinState *ginstate, OffsetNumber attnum,
 		return 0;
 
 	/* both not null, so safe to call the compareFn */
-	return DatumGetInt32(FunctionCall2Coll(&ginstate->compareFn[attnum - 1],
+	return DatumGetInt32(FunctionCall3Coll(&ginstate->compareFn[attnum - 1],
 										   ginstate->supportCollation[attnum - 1],
-										   a, b));
+										   a, b,
+										   PointerGetDatum(ginstate->opclassOptions[attnum - 1])));
 }
 
 /*
@@ -437,6 +440,7 @@ typedef struct
 {
 	FmgrInfo   *cmpDatumFunc;
 	Oid			collation;
+	Datum		options;
 	bool		haveDups;
 } cmpEntriesArg;
 
@@ -458,9 +462,10 @@ cmpEntries(const void *a, const void *b, void *arg)
 	else if (bb->isnull)
 		res = -1;				/* not-NULL "<" NULL */
 	else
-		res = DatumGetInt32(FunctionCall2Coll(data->cmpDatumFunc,
+		res = DatumGetInt32(FunctionCall3Coll(data->cmpDatumFunc,
 											  data->collation,
-											  aa->datum, bb->datum));
+											  aa->datum, bb->datum,
+											  data->options));
 
 	/*
 	 * Detect if we have any duplicates.  If there are equal keys, qsort must
@@ -506,11 +511,12 @@ ginExtractEntries(GinState *ginstate, OffsetNumber attnum,
 	/* OK, call the opclass's extractValueFn */
 	nullFlags = NULL;			/* in case extractValue doesn't set it */
 	entries = (Datum *)
-		DatumGetPointer(FunctionCall3Coll(&ginstate->extractValueFn[attnum - 1],
+		DatumGetPointer(FunctionCall4Coll(&ginstate->extractValueFn[attnum - 1],
 										  ginstate->supportCollation[attnum - 1],
 										  value,
 										  PointerGetDatum(nentries),
-										  PointerGetDatum(&nullFlags)));
+										  PointerGetDatum(&nullFlags),
+										  PointerGetDatum(ginstate->opclassOptions[attnum - 1])));
 
 	/*
 	 * Generate a placeholder if the item contained no keys.
@@ -553,6 +559,7 @@ ginExtractEntries(GinState *ginstate, OffsetNumber attnum,
 
 		arg.cmpDatumFunc = &ginstate->compareFn[attnum - 1];
 		arg.collation = ginstate->supportCollation[attnum - 1];
+		arg.options = PointerGetDatum(ginstate->opclassOptions[attnum - 1]);
 		arg.haveDups = false;
 		qsort_arg(keydata, *nentries, sizeof(keyEntryData),
 				  cmpEntries, (void *) &arg);
@@ -628,6 +635,14 @@ ginoptions(Datum reloptions, bool validate)
 	return (bytea *) rdopts;
 }
 
+bytea *
+ginopclassoptions(Relation index, AttrNumber colno, Datum attoptions,
+				  bool validate)
+{
+	return index_opclass_options_generic(index, colno, GIN_OPCLASSOPTIONS_PROC,
+										 attoptions, validate);
+}
+
 /*
  * Fetch index's statistical data into *stats
  *
diff --git a/src/backend/access/gin/ginvalidate.c b/src/backend/access/gin/ginvalidate.c
index 63bd7f2adc..a000052f09 100644
--- a/src/backend/access/gin/ginvalidate.c
+++ b/src/backend/access/gin/ginvalidate.c
@@ -108,40 +108,47 @@ ginvalidate(Oid opclassoid)
 		{
 			case GIN_COMPARE_PROC:
 				ok = check_amproc_signature(procform->amproc, INT4OID, false,
-											2, 2, opckeytype, opckeytype);
+											2, 3, opckeytype, opckeytype,
+											INTERNALOID);
 				break;
 			case GIN_EXTRACTVALUE_PROC:
 				/* Some opclasses omit nullFlags */
 				ok = check_amproc_signature(procform->amproc, INTERNALOID, false,
-											2, 3, opcintype, INTERNALOID,
-											INTERNALOID);
+											2, 4, opcintype, INTERNALOID,
+											INTERNALOID, INTERNALOID);
 				break;
 			case GIN_EXTRACTQUERY_PROC:
 				/* Some opclasses omit nullFlags and searchMode */
 				ok = check_amproc_signature(procform->amproc, INTERNALOID, false,
-											5, 7, opcintype, INTERNALOID,
+											5, 8, opcintype, INTERNALOID,
 											INT2OID, INTERNALOID, INTERNALOID,
-											INTERNALOID, INTERNALOID);
+											INTERNALOID, INTERNALOID,
+											INTERNALOID);
 				break;
 			case GIN_CONSISTENT_PROC:
 				/* Some opclasses omit queryKeys and nullFlags */
 				ok = check_amproc_signature(procform->amproc, BOOLOID, false,
-											6, 8, INTERNALOID, INT2OID,
+											6, 9, INTERNALOID, INT2OID,
 											opcintype, INT4OID,
 											INTERNALOID, INTERNALOID,
-											INTERNALOID, INTERNALOID);
+											INTERNALOID, INTERNALOID,
+											INTERNALOID);
 				break;
 			case GIN_COMPARE_PARTIAL_PROC:
 				ok = check_amproc_signature(procform->amproc, INT4OID, false,
-											4, 4, opckeytype, opckeytype,
-											INT2OID, INTERNALOID);
+											4, 5, opckeytype, opckeytype,
+											INT2OID, INTERNALOID, INTERNALOID);
 				break;
 			case GIN_TRICONSISTENT_PROC:
 				ok = check_amproc_signature(procform->amproc, CHAROID, false,
-											7, 7, INTERNALOID, INT2OID,
+											7, 8, INTERNALOID, INT2OID,
 											opcintype, INT4OID,
 											INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											INTERNALOID, INTERNALOID);
+				break;
+			case GIN_OPCLASSOPTIONS_PROC:
+				ok = check_amproc_signature(procform->amproc, INTERNALOID,
+											false, 2, 2, INTERNALOID, BOOLOID);
 				break;
 			default:
 				ereport(INFO,
@@ -238,7 +245,8 @@ ginvalidate(Oid opclassoid)
 		if (opclassgroup &&
 			(opclassgroup->functionset & (((uint64) 1) << i)) != 0)
 			continue;			/* got it */
-		if (i == GIN_COMPARE_PROC || i == GIN_COMPARE_PARTIAL_PROC)
+		if (i == GIN_COMPARE_PROC || i == GIN_COMPARE_PARTIAL_PROC ||
+			i == GIN_OPCLASSOPTIONS_PROC)
 			continue;			/* optional method */
 		if (i == GIN_CONSISTENT_PROC || i == GIN_TRICONSISTENT_PROC)
 			continue;			/* don't need both, see check below loop */
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d7e3f09f1a..d491bdd7ae 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -135,6 +135,7 @@
 #include "utils/lsyscache.h"
 #include "utils/pg_locale.h"
 #include "utils/rel.h"
+#include "utils/relcache.h"
 #include "utils/selfuncs.h"
 #include "utils/snapmgr.h"
 #include "utils/spccache.h"
@@ -6242,7 +6243,7 @@ gincost_pattern(IndexOptInfo *index, int indexcol,
 	else
 		collation = DEFAULT_COLLATION_OID;
 
-	OidFunctionCall7Coll(extractProcOid,
+	OidFunctionCall8Coll(extractProcOid,
 						 collation,
 						 query,
 						 PointerGetDatum(&nentries),
@@ -6250,7 +6251,8 @@ gincost_pattern(IndexOptInfo *index, int indexcol,
 						 PointerGetDatum(&partial_matches),
 						 PointerGetDatum(&extra_data),
 						 PointerGetDatum(&nullFlags),
-						 PointerGetDatum(&searchMode));
+						 PointerGetDatum(&searchMode),
+						 PointerGetDatum(index->opclassoptions[indexcol]));
 
 	if (nentries <= 0 && searchMode == GIN_SEARCH_MODE_DEFAULT)
 	{
diff --git a/src/include/access/gin.h b/src/include/access/gin.h
index a8eef5a379..aabfbcd5ce 100644
--- a/src/include/access/gin.h
+++ b/src/include/access/gin.h
@@ -25,7 +25,8 @@
 #define GIN_CONSISTENT_PROC			   4
 #define GIN_COMPARE_PARTIAL_PROC	   5
 #define GIN_TRICONSISTENT_PROC		   6
-#define GINNProcs					   6
+#define GIN_OPCLASSOPTIONS_PROC		   7
+#define GINNProcs					   7
 
 /*
  * searchMode settings for extractQueryFn.
diff --git a/src/include/access/gin_private.h b/src/include/access/gin_private.h
index afb3e15721..0f626a7dbb 100644
--- a/src/include/access/gin_private.h
+++ b/src/include/access/gin_private.h
@@ -67,6 +67,8 @@ typedef struct GinState
 	TupleDesc	origTupdesc;
 	TupleDesc	tupdesc[INDEX_MAX_KEYS];
 
+	bytea	  **opclassOptions;	/* per-index-column opclass options */
+
 	/*
 	 * Per-index-column opclass support functions
 	 */
@@ -85,6 +87,8 @@ typedef struct GinState
 
 /* ginutil.c */
 extern bytea *ginoptions(Datum reloptions, bool validate);
+extern bytea *ginopclassoptions(Relation index, AttrNumber colno,
+				  Datum attoptions, bool validate);
 extern void initGinState(GinState *state, Relation index);
 extern Buffer GinNewBuffer(Relation index);
 extern void GinInitBuffer(Buffer b, uint32 f);
@@ -297,6 +301,7 @@ typedef struct GinScanKeyData
 	StrategyNumber strategy;
 	int32		searchMode;
 	OffsetNumber attnum;
+	bytea	   *opclassOptions;
 
 	/*
 	 * Match status data.  curItem is the TID most recently tested (could be a
-- 
2.20.1

0004-Add-opclass-parameters-to-GiST-tsvector_ops-20190611.patchtext/plain; charset=us-asciiDownload
From ae97174555cbaf059d2df45df3c80d7e67007fdd Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 21:38:42 +0200
Subject: [PATCH 04/10] Add opclass parameters to GiST tsvector_ops

---
 doc/src/sgml/textsearch.sgml          |   9 +-
 src/backend/utils/adt/tsgistidx.c     | 269 ++++++++++++++------------
 src/include/catalog/pg_amproc.dat     |   5 +-
 src/include/catalog/pg_proc.dat       |  19 +-
 src/test/regress/expected/tsearch.out | 176 +++++++++++++++++
 src/test/regress/sql/tsearch.sql      |  45 +++++
 6 files changed, 392 insertions(+), 131 deletions(-)

diff --git a/doc/src/sgml/textsearch.sgml b/doc/src/sgml/textsearch.sgml
index 40888a4d20..54b796ecf1 100644
--- a/doc/src/sgml/textsearch.sgml
+++ b/doc/src/sgml/textsearch.sgml
@@ -3637,7 +3637,7 @@ SELECT plainto_tsquery('supernovae stars');
       <tertiary>text search</tertiary>
      </indexterm>
 
-      <literal>CREATE INDEX <replaceable>name</replaceable> ON <replaceable>table</replaceable> USING GIST (<replaceable>column</replaceable>);</literal>
+      <literal>CREATE INDEX <replaceable>name</replaceable> ON <replaceable>table</replaceable> USING GIST (<replaceable>column</replaceable> [ { DEFAULT | tsvector_ops } (siglen = <replaceable>number</replaceable>) ] );</literal>
      </term>
 
      <listitem>
@@ -3645,6 +3645,8 @@ SELECT plainto_tsquery('supernovae stars');
        Creates a GiST (Generalized Search Tree)-based index.
        The <replaceable>column</replaceable> can be of <type>tsvector</type> or
        <type>tsquery</type> type.
+       Optional integer parameter <literal>siglen</literal> determines
+       signature length in bytes (see below for details).
       </para>
      </listitem>
     </varlistentry>
@@ -3668,7 +3670,10 @@ SELECT plainto_tsquery('supernovae stars');
    to check the actual table row to eliminate such false matches.
    (<productname>PostgreSQL</productname> does this automatically when needed.)
    GiST indexes are lossy because each document is represented in the
-   index by a fixed-length signature. The signature is generated by hashing
+   index by a fixed-length signature.  Signature length in bytes is determined
+   by the value of the optional integer parameter <literal>siglen</literal>.
+   Default signature length (when <literal>siglen</literal> is not specied) is
+   124 bytes, maximal length is 484 bytes. The signature is generated by hashing
    each word into a single bit in an n-bit string, with all these bits OR-ed
    together to produce an n-bit document signature.  When two words hash to
    the same bit position there will be a false match.  If all words in
diff --git a/src/backend/utils/adt/tsgistidx.c b/src/backend/utils/adt/tsgistidx.c
index 4f256260fd..91661cf8b8 100644
--- a/src/backend/utils/adt/tsgistidx.c
+++ b/src/backend/utils/adt/tsgistidx.c
@@ -15,6 +15,7 @@
 #include "postgres.h"
 
 #include "access/gist.h"
+#include "access/reloptions.h"
 #include "access/tuptoaster.h"
 #include "port/pg_bitutils.h"
 #include "tsearch/ts_utils.h"
@@ -22,17 +23,22 @@
 #include "utils/pg_crc.h"
 
 
-#define SIGLENINT  31			/* >121 => key will toast, so it will not work
-								 * !!! */
+#define SIGLEN_DEFAULT	(31 * 4)
+#define SIGLEN_MAX		(121 * 4)	/* key will toast, so it will not work !!! */
 
-#define SIGLEN	( sizeof(int32) * SIGLENINT )
-#define SIGLENBIT (SIGLEN * BITS_PER_BYTE)
+#define SIGLENBIT(siglen) ((siglen) * BITS_PER_BYTE)
+
+/* tsvector_ops opclass options */
+typedef struct GistTsVectorOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			siglen;			/* signature length */
+}	GistTsVectorOptions;
 
-typedef char BITVEC[SIGLEN];
 typedef char *BITVECP;
 
-#define LOOPBYTE \
-			for(i=0;i<SIGLEN;i++)
+#define LOOPBYTE(siglen) \
+			for (i = 0; i < siglen; i++)
 
 #define GETBYTE(x,i) ( *( (BITVECP)(x) + (int)( (i) / BITS_PER_BYTE ) ) )
 #define GETBITBYTE(x,i) ( ((char)(x)) >> (i) & 0x01 )
@@ -40,8 +46,8 @@ typedef char *BITVECP;
 #define SETBIT(x,i)   GETBYTE(x,i) |=  ( 0x01 << ( (i) % BITS_PER_BYTE ) )
 #define GETBIT(x,i) ( (GETBYTE(x,i) >> ( (i) % BITS_PER_BYTE )) & 0x01 )
 
-#define HASHVAL(val) (((unsigned int)(val)) % SIGLENBIT)
-#define HASH(sign, val) SETBIT((sign), HASHVAL(val))
+#define HASHVAL(val, siglen) (((unsigned int)(val)) % SIGLENBIT(siglen))
+#define HASH(sign, val, siglen) SETBIT((sign), HASHVAL(val, siglen))
 
 #define GETENTRY(vec,pos) ((SignTSVector *) DatumGetPointer((vec)->vector[(pos)].key))
 
@@ -65,13 +71,14 @@ typedef struct
 #define ISALLTRUE(x)	( ((SignTSVector*)(x))->flag & ALLISTRUE )
 
 #define GTHDRSIZE	( VARHDRSZ + sizeof(int32) )
-#define CALCGTSIZE(flag, len) ( GTHDRSIZE + ( ( (flag) & ARRKEY ) ? ((len)*sizeof(int32)) : (((flag) & ALLISTRUE) ? 0 : SIGLEN) ) )
+#define CALCGTSIZE(flag, len) ( GTHDRSIZE + ( ( (flag) & ARRKEY ) ? ((len)*sizeof(int32)) : (((flag) & ALLISTRUE) ? 0 : (len)) ) )
 
 #define GETSIGN(x)	( (BITVECP)( (char*)(x)+GTHDRSIZE ) )
+#define GETSIGLEN(x)( VARSIZE(x) - GTHDRSIZE )
 #define GETARR(x)	( (int32*)( (char*)(x)+GTHDRSIZE ) )
 #define ARRNELEM(x) ( ( VARSIZE(x) - GTHDRSIZE )/sizeof(int32) )
 
-static int32 sizebitvec(BITVECP sign);
+static int32 sizebitvec(BITVECP sign, int siglen);
 
 Datum
 gtsvectorin(PG_FUNCTION_ARGS)
@@ -102,9 +109,10 @@ gtsvectorout(PG_FUNCTION_ARGS)
 		sprintf(outbuf, ARROUTSTR, (int) ARRNELEM(key));
 	else
 	{
-		int			cnttrue = (ISALLTRUE(key)) ? SIGLENBIT : sizebitvec(GETSIGN(key));
+		int			siglen = GETSIGLEN(key);
+		int			cnttrue = (ISALLTRUE(key)) ? SIGLENBIT(siglen) : sizebitvec(GETSIGN(key), siglen);
 
-		sprintf(outbuf, SINGOUTSTR, cnttrue, (int) SIGLENBIT - cnttrue);
+		sprintf(outbuf, SINGOUTSTR, cnttrue, (int) SIGLENBIT(siglen) - cnttrue);
 	}
 
 	PG_FREE_IF_COPY(key, 0);
@@ -148,36 +156,49 @@ uniqueint(int32 *a, int32 l)
 }
 
 static void
-makesign(BITVECP sign, SignTSVector *a)
+makesign(BITVECP sign, SignTSVector *a, int siglen)
 {
 	int32		k,
 				len = ARRNELEM(a);
 	int32	   *ptr = GETARR(a);
 
-	MemSet((void *) sign, 0, sizeof(BITVEC));
+	MemSet((void *) sign, 0, siglen);
 	for (k = 0; k < len; k++)
-		HASH(sign, ptr[k]);
+		HASH(sign, ptr[k], siglen);
+}
+
+static SignTSVector *
+gtsvector_alloc(int flag, int len, BITVECP sign)
+{
+	int			size = CALCGTSIZE(flag, len);
+	SignTSVector *res = palloc(size);
+
+	SET_VARSIZE(res, size);
+	res->flag = flag;
+
+	if ((flag & (SIGNKEY | ALLISTRUE)) == SIGNKEY && sign)
+		memcpy(GETSIGN(res), sign, len);
+
+	return res;
 }
 
+
 Datum
 gtsvector_compress(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
+	int			siglen = ((GistTsVectorOptions *) PG_GETARG_POINTER(1))->siglen;
 	GISTENTRY  *retval = entry;
 
 	if (entry->leafkey)
 	{							/* tsvector */
-		SignTSVector *res;
 		TSVector	val = DatumGetTSVector(entry->key);
+		SignTSVector *res = gtsvector_alloc(ARRKEY, val->size, NULL);
 		int32		len;
 		int32	   *arr;
 		WordEntry  *ptr = ARRPTR(val);
 		char	   *words = STRPTR(val);
 
-		len = CALCGTSIZE(ARRKEY, val->size);
-		res = (SignTSVector *) palloc(len);
-		SET_VARSIZE(res, len);
-		res->flag = ARRKEY;
 		arr = GETARR(res);
 		len = val->size;
 		while (len--)
@@ -208,13 +229,9 @@ gtsvector_compress(PG_FUNCTION_ARGS)
 		/* make signature, if array is too long */
 		if (VARSIZE(res) > TOAST_INDEX_TARGET)
 		{
-			SignTSVector *ressign;
+			SignTSVector *ressign = gtsvector_alloc(SIGNKEY, siglen, NULL);
 
-			len = CALCGTSIZE(SIGNKEY, 0);
-			ressign = (SignTSVector *) palloc(len);
-			SET_VARSIZE(ressign, len);
-			ressign->flag = SIGNKEY;
-			makesign(GETSIGN(ressign), res);
+			makesign(GETSIGN(ressign), res, siglen);
 			res = ressign;
 		}
 
@@ -226,22 +243,17 @@ gtsvector_compress(PG_FUNCTION_ARGS)
 	else if (ISSIGNKEY(DatumGetPointer(entry->key)) &&
 			 !ISALLTRUE(DatumGetPointer(entry->key)))
 	{
-		int32		i,
-					len;
+		int32		i;
 		SignTSVector *res;
 		BITVECP		sign = GETSIGN(DatumGetPointer(entry->key));
 
-		LOOPBYTE
+		LOOPBYTE(siglen)
 		{
 			if ((sign[i] & 0xff) != 0xff)
 				PG_RETURN_POINTER(retval);
 		}
 
-		len = CALCGTSIZE(SIGNKEY | ALLISTRUE, 0);
-		res = (SignTSVector *) palloc(len);
-		SET_VARSIZE(res, len);
-		res->flag = SIGNKEY | ALLISTRUE;
-
+		res = gtsvector_alloc(SIGNKEY | ALLISTRUE, siglen, sign);
 		retval = (GISTENTRY *) palloc(sizeof(GISTENTRY));
 		gistentryinit(*retval, PointerGetDatum(res),
 					  entry->rel, entry->page,
@@ -315,12 +327,14 @@ checkcondition_arr(void *checkval, QueryOperand *val, ExecPhraseData *data)
 static bool
 checkcondition_bit(void *checkval, QueryOperand *val, ExecPhraseData *data)
 {
+	void *key = (SignTSVector *) checkval;
+
 	/*
 	 * we are not able to find a prefix in signature tree
 	 */
 	if (val->prefix)
 		return true;
-	return GETBIT(checkval, HASHVAL(val->valcrc));
+	return GETBIT(GETSIGN(key), HASHVAL(val->valcrc, GETSIGLEN(key)));
 }
 
 Datum
@@ -347,7 +361,7 @@ gtsvector_consistent(PG_FUNCTION_ARGS)
 
 		/* since signature is lossy, cannot specify CALC_NOT here */
 		PG_RETURN_BOOL(TS_execute(GETQUERY(query),
-								  (void *) GETSIGN(key),
+								  key,
 								  TS_EXEC_PHRASE_NO_POS,
 								  checkcondition_bit));
 	}
@@ -365,7 +379,7 @@ gtsvector_consistent(PG_FUNCTION_ARGS)
 }
 
 static int32
-unionkey(BITVECP sbase, SignTSVector *add)
+unionkey(BITVECP sbase, SignTSVector *add, int siglen)
 {
 	int32		i;
 
@@ -376,7 +390,9 @@ unionkey(BITVECP sbase, SignTSVector *add)
 		if (ISALLTRUE(add))
 			return 1;
 
-		LOOPBYTE
+		Assert(GETSIGLEN(add) == siglen);
+
+		LOOPBYTE(siglen)
 			sbase[i] |= sadd[i];
 	}
 	else
@@ -384,7 +400,7 @@ unionkey(BITVECP sbase, SignTSVector *add)
 		int32	   *ptr = GETARR(add);
 
 		for (i = 0; i < ARRNELEM(add); i++)
-			HASH(sbase, ptr[i]);
+			HASH(sbase, ptr[i], siglen);
 	}
 	return 0;
 }
@@ -395,30 +411,24 @@ gtsvector_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
 	int		   *size = (int *) PG_GETARG_POINTER(1);
-	BITVEC		base;
-	int32		i,
-				len;
-	int32		flag = 0;
-	SignTSVector *result;
+	int			siglen = ((GistTsVectorOptions *) PG_GETARG_POINTER(2))->siglen;
+	SignTSVector *result = gtsvector_alloc(SIGNKEY, siglen, NULL);
+	BITVECP		base = GETSIGN(result);
+	int32		i;
+
+	memset(base, 0, siglen);
 
-	MemSet((void *) base, 0, sizeof(BITVEC));
 	for (i = 0; i < entryvec->n; i++)
 	{
-		if (unionkey(base, GETENTRY(entryvec, i)))
+		if (unionkey(base, GETENTRY(entryvec, i), siglen))
 		{
-			flag = ALLISTRUE;
+			result->flag |= ALLISTRUE;
+			SET_VARSIZE(result, CALCGTSIZE(result->flag, siglen));
 			break;
 		}
 	}
 
-	flag |= SIGNKEY;
-	len = CALCGTSIZE(flag, 0);
-	result = (SignTSVector *) palloc(len);
-	*size = len;
-	SET_VARSIZE(result, len);
-	result->flag = flag;
-	if (!ISALLTRUE(result))
-		memcpy((void *) GETSIGN(result), (void *) base, sizeof(BITVEC));
+	*size = VARSIZE(result);
 
 	PG_RETURN_POINTER(result);
 }
@@ -429,6 +439,7 @@ gtsvector_same(PG_FUNCTION_ARGS)
 	SignTSVector *a = (SignTSVector *) PG_GETARG_POINTER(0);
 	SignTSVector *b = (SignTSVector *) PG_GETARG_POINTER(1);
 	bool	   *result = (bool *) PG_GETARG_POINTER(2);
+	int			siglen = ((GistTsVectorOptions *) PG_GETARG_POINTER(3))->siglen;
 
 	if (ISSIGNKEY(a))
 	{							/* then b also ISSIGNKEY */
@@ -444,8 +455,10 @@ gtsvector_same(PG_FUNCTION_ARGS)
 			BITVECP		sa = GETSIGN(a),
 						sb = GETSIGN(b);
 
+			Assert(GETSIGLEN(a) == siglen && GETSIGLEN(b) == siglen);
+
 			*result = true;
-			LOOPBYTE
+			LOOPBYTE(siglen)
 			{
 				if (sa[i] != sb[i])
 				{
@@ -482,19 +495,19 @@ gtsvector_same(PG_FUNCTION_ARGS)
 }
 
 static int32
-sizebitvec(BITVECP sign)
+sizebitvec(BITVECP sign, int siglen)
 {
-	return pg_popcount(sign, SIGLEN);
+	return pg_popcount(sign, siglen);
 }
 
 static int
-hemdistsign(BITVECP a, BITVECP b)
+hemdistsign(BITVECP a, BITVECP b, int siglen)
 {
 	int			i,
 				diff,
 				dist = 0;
 
-	LOOPBYTE
+	LOOPBYTE(siglen)
 	{
 		diff = (unsigned char) (a[i] ^ b[i]);
 		/* Using the popcount functions here isn't likely to win */
@@ -506,17 +519,22 @@ hemdistsign(BITVECP a, BITVECP b)
 static int
 hemdist(SignTSVector *a, SignTSVector *b)
 {
+	int siglena = GETSIGLEN(a);
+	int siglenb = GETSIGLEN(b);
+
 	if (ISALLTRUE(a))
 	{
 		if (ISALLTRUE(b))
 			return 0;
 		else
-			return SIGLENBIT - sizebitvec(GETSIGN(b));
+			return SIGLENBIT(siglenb) - sizebitvec(GETSIGN(b), siglenb);
 	}
 	else if (ISALLTRUE(b))
-		return SIGLENBIT - sizebitvec(GETSIGN(a));
+		return SIGLENBIT(siglena) - sizebitvec(GETSIGN(a), siglena);
 
-	return hemdistsign(GETSIGN(a), GETSIGN(b));
+	Assert(siglena == siglenb);
+
+	return hemdistsign(GETSIGN(a), GETSIGN(b), siglena);
 }
 
 Datum
@@ -525,6 +543,7 @@ gtsvector_penalty(PG_FUNCTION_ARGS)
 	GISTENTRY  *origentry = (GISTENTRY *) PG_GETARG_POINTER(0); /* always ISSIGNKEY */
 	GISTENTRY  *newentry = (GISTENTRY *) PG_GETARG_POINTER(1);
 	float	   *penalty = (float *) PG_GETARG_POINTER(2);
+	int			siglen = ((GistTsVectorOptions *) PG_GETARG_POINTER(3))->siglen;
 	SignTSVector *origval = (SignTSVector *) DatumGetPointer(origentry->key);
 	SignTSVector *newval = (SignTSVector *) DatumGetPointer(newentry->key);
 	BITVECP		orig = GETSIGN(origval);
@@ -533,14 +552,22 @@ gtsvector_penalty(PG_FUNCTION_ARGS)
 
 	if (ISARRKEY(newval))
 	{
-		BITVEC		sign;
+		BITVECP		sign = palloc(siglen);
 
-		makesign(sign, newval);
+		makesign(sign, newval, siglen);
 
 		if (ISALLTRUE(origval))
-			*penalty = ((float) (SIGLENBIT - sizebitvec(sign))) / (float) (SIGLENBIT + 1);
+		{
+			int			siglenbit = SIGLENBIT(siglen);
+
+			*penalty =
+				(float) (siglenbit - sizebitvec(sign, siglen)) /
+				(float) (siglenbit + 1);
+		}
 		else
-			*penalty = hemdistsign(sign, orig);
+			*penalty = hemdistsign(sign, orig, siglen);
+
+		pfree(sign);
 	}
 	else
 		*penalty = hemdist(origval, newval);
@@ -550,19 +577,19 @@ gtsvector_penalty(PG_FUNCTION_ARGS)
 typedef struct
 {
 	bool		allistrue;
-	BITVEC		sign;
+	BITVECP		sign;
 } CACHESIGN;
 
 static void
-fillcache(CACHESIGN *item, SignTSVector *key)
+fillcache(CACHESIGN *item, SignTSVector *key, int siglen)
 {
 	item->allistrue = false;
 	if (ISARRKEY(key))
-		makesign(item->sign, key);
+		makesign(item->sign, key, siglen);
 	else if (ISALLTRUE(key))
 		item->allistrue = true;
 	else
-		memcpy((void *) item->sign, (void *) GETSIGN(key), sizeof(BITVEC));
+		memcpy((void *) item->sign, (void *) GETSIGN(key), siglen);
 }
 
 #define WISH_F(a,b,c) (double)( -(double)(((a)-(b))*((a)-(b))*((a)-(b)))*(c) )
@@ -586,19 +613,19 @@ comparecost(const void *va, const void *vb)
 
 
 static int
-hemdistcache(CACHESIGN *a, CACHESIGN *b)
+hemdistcache(CACHESIGN *a, CACHESIGN *b, int siglen)
 {
 	if (a->allistrue)
 	{
 		if (b->allistrue)
 			return 0;
 		else
-			return SIGLENBIT - sizebitvec(b->sign);
+			return SIGLENBIT(siglen) - sizebitvec(b->sign, siglen);
 	}
 	else if (b->allistrue)
-		return SIGLENBIT - sizebitvec(a->sign);
+		return SIGLENBIT(siglen) - sizebitvec(a->sign, siglen);
 
-	return hemdistsign(a->sign, b->sign);
+	return hemdistsign(a->sign, b->sign, siglen);
 }
 
 Datum
@@ -606,6 +633,7 @@ gtsvector_picksplit(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
 	GIST_SPLITVEC *v = (GIST_SPLITVEC *) PG_GETARG_POINTER(1);
+	int			siglen = ((GistTsVectorOptions *) PG_GETARG_POINTER(2))->siglen;
 	OffsetNumber k,
 				j;
 	SignTSVector *datum_l,
@@ -625,6 +653,7 @@ gtsvector_picksplit(PG_FUNCTION_ARGS)
 	BITVECP		ptr;
 	int			i;
 	CACHESIGN  *cache;
+	char	   *cache_sign;
 	SPLITCOST  *costvector;
 
 	maxoff = entryvec->n - 2;
@@ -633,16 +662,22 @@ gtsvector_picksplit(PG_FUNCTION_ARGS)
 	v->spl_right = (OffsetNumber *) palloc(nbytes);
 
 	cache = (CACHESIGN *) palloc(sizeof(CACHESIGN) * (maxoff + 2));
-	fillcache(&cache[FirstOffsetNumber], GETENTRY(entryvec, FirstOffsetNumber));
+	cache_sign = palloc(siglen * (maxoff + 2));
+
+	for (j = 0; j < maxoff + 2; j++)
+		cache[j].sign = &cache_sign[siglen * j];
+
+	fillcache(&cache[FirstOffsetNumber], GETENTRY(entryvec, FirstOffsetNumber),
+			  siglen);
 
 	for (k = FirstOffsetNumber; k < maxoff; k = OffsetNumberNext(k))
 	{
 		for (j = OffsetNumberNext(k); j <= maxoff; j = OffsetNumberNext(j))
 		{
 			if (k == FirstOffsetNumber)
-				fillcache(&cache[j], GETENTRY(entryvec, j));
+				fillcache(&cache[j], GETENTRY(entryvec, j), siglen);
 
-			size_waste = hemdistcache(&(cache[j]), &(cache[k]));
+			size_waste = hemdistcache(&(cache[j]), &(cache[k]), siglen);
 			if (size_waste > waste)
 			{
 				waste = size_waste;
@@ -664,44 +699,21 @@ gtsvector_picksplit(PG_FUNCTION_ARGS)
 	}
 
 	/* form initial .. */
-	if (cache[seed_1].allistrue)
-	{
-		datum_l = (SignTSVector *) palloc(CALCGTSIZE(SIGNKEY | ALLISTRUE, 0));
-		SET_VARSIZE(datum_l, CALCGTSIZE(SIGNKEY | ALLISTRUE, 0));
-		datum_l->flag = SIGNKEY | ALLISTRUE;
-	}
-	else
-	{
-		datum_l = (SignTSVector *) palloc(CALCGTSIZE(SIGNKEY, 0));
-		SET_VARSIZE(datum_l, CALCGTSIZE(SIGNKEY, 0));
-		datum_l->flag = SIGNKEY;
-		memcpy((void *) GETSIGN(datum_l), (void *) cache[seed_1].sign, sizeof(BITVEC));
-	}
-	if (cache[seed_2].allistrue)
-	{
-		datum_r = (SignTSVector *) palloc(CALCGTSIZE(SIGNKEY | ALLISTRUE, 0));
-		SET_VARSIZE(datum_r, CALCGTSIZE(SIGNKEY | ALLISTRUE, 0));
-		datum_r->flag = SIGNKEY | ALLISTRUE;
-	}
-	else
-	{
-		datum_r = (SignTSVector *) palloc(CALCGTSIZE(SIGNKEY, 0));
-		SET_VARSIZE(datum_r, CALCGTSIZE(SIGNKEY, 0));
-		datum_r->flag = SIGNKEY;
-		memcpy((void *) GETSIGN(datum_r), (void *) cache[seed_2].sign, sizeof(BITVEC));
-	}
-
+	datum_l = gtsvector_alloc(SIGNKEY | (cache[seed_1].allistrue ? ALLISTRUE : 0),
+							  siglen, cache[seed_1].sign);
+	datum_r = gtsvector_alloc(SIGNKEY | (cache[seed_2].allistrue ? ALLISTRUE : 0),
+							  siglen, cache[seed_2].sign);
 	union_l = GETSIGN(datum_l);
 	union_r = GETSIGN(datum_r);
 	maxoff = OffsetNumberNext(maxoff);
-	fillcache(&cache[maxoff], GETENTRY(entryvec, maxoff));
+	fillcache(&cache[maxoff], GETENTRY(entryvec, maxoff), siglen);
 	/* sort before ... */
 	costvector = (SPLITCOST *) palloc(sizeof(SPLITCOST) * maxoff);
 	for (j = FirstOffsetNumber; j <= maxoff; j = OffsetNumberNext(j))
 	{
 		costvector[j - 1].pos = j;
-		size_alpha = hemdistcache(&(cache[seed_1]), &(cache[j]));
-		size_beta = hemdistcache(&(cache[seed_2]), &(cache[j]));
+		size_alpha = hemdistcache(&(cache[seed_1]), &(cache[j]), siglen);
+		size_beta = hemdistcache(&(cache[seed_2]), &(cache[j]), siglen);
 		costvector[j - 1].cost = Abs(size_alpha - size_beta);
 	}
 	qsort((void *) costvector, maxoff, sizeof(SPLITCOST), comparecost);
@@ -727,36 +739,34 @@ gtsvector_picksplit(PG_FUNCTION_ARGS)
 			if (ISALLTRUE(datum_l) && cache[j].allistrue)
 				size_alpha = 0;
 			else
-				size_alpha = SIGLENBIT - sizebitvec(
-													(cache[j].allistrue) ? GETSIGN(datum_l) : GETSIGN(cache[j].sign)
-					);
+				size_alpha = SIGLENBIT(siglen) -
+					sizebitvec((cache[j].allistrue) ? GETSIGN(datum_l) : GETSIGN(cache[j].sign), siglen);
 		}
 		else
-			size_alpha = hemdistsign(cache[j].sign, GETSIGN(datum_l));
+			size_alpha = hemdistsign(cache[j].sign, GETSIGN(datum_l), siglen);
 
 		if (ISALLTRUE(datum_r) || cache[j].allistrue)
 		{
 			if (ISALLTRUE(datum_r) && cache[j].allistrue)
 				size_beta = 0;
 			else
-				size_beta = SIGLENBIT - sizebitvec(
-												   (cache[j].allistrue) ? GETSIGN(datum_r) : GETSIGN(cache[j].sign)
-					);
+				size_beta = SIGLENBIT(siglen) -
+					sizebitvec((cache[j].allistrue) ? GETSIGN(datum_r) : GETSIGN(cache[j].sign), siglen);
 		}
 		else
-			size_beta = hemdistsign(cache[j].sign, GETSIGN(datum_r));
+			size_beta = hemdistsign(cache[j].sign, GETSIGN(datum_r), siglen);
 
 		if (size_alpha < size_beta + WISH_F(v->spl_nleft, v->spl_nright, 0.1))
 		{
 			if (ISALLTRUE(datum_l) || cache[j].allistrue)
 			{
 				if (!ISALLTRUE(datum_l))
-					MemSet((void *) GETSIGN(datum_l), 0xff, sizeof(BITVEC));
+					MemSet((void *) GETSIGN(datum_l), 0xff, siglen);
 			}
 			else
 			{
 				ptr = cache[j].sign;
-				LOOPBYTE
+				LOOPBYTE(siglen)
 					union_l[i] |= ptr[i];
 			}
 			*left++ = j;
@@ -767,12 +777,12 @@ gtsvector_picksplit(PG_FUNCTION_ARGS)
 			if (ISALLTRUE(datum_r) || cache[j].allistrue)
 			{
 				if (!ISALLTRUE(datum_r))
-					MemSet((void *) GETSIGN(datum_r), 0xff, sizeof(BITVEC));
+					MemSet((void *) GETSIGN(datum_r), 0xff, siglen);
 			}
 			else
 			{
 				ptr = cache[j].sign;
-				LOOPBYTE
+				LOOPBYTE(siglen)
 					union_r[i] |= ptr[i];
 			}
 			*right++ = j;
@@ -799,3 +809,20 @@ gtsvector_consistent_oldsig(PG_FUNCTION_ARGS)
 {
 	return gtsvector_consistent(fcinfo);
 }
+
+Datum
+gtsvector_options(PG_FUNCTION_ARGS)
+{
+	Datum		raw_options = PG_GETARG_DATUM(0);
+	bool		validate = PG_GETARG_BOOL(1);
+	relopt_int	siglen =
+		{ {"siglen", "signature length", 0, 0, 6, RELOPT_TYPE_INT },
+			SIGLEN_DEFAULT, 1, SIGLEN_MAX };
+	relopt_gen *optgen[] = { &siglen.gen };
+	int			offsets[] = { offsetof(GistTsVectorOptions, siglen) };
+	GistTsVectorOptions *options =
+		parseAndFillLocalRelOptions(raw_options, optgen, offsets, 1,
+									sizeof(GistTsVectorOptions), validate);
+
+	PG_RETURN_POINTER(options);
+}
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 020b7413cc..5ceee11ab1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -458,7 +458,7 @@
   amproc => 'gist_circle_distance' },
 { amprocfamily => 'gist/tsvector_ops', amproclefttype => 'tsvector',
   amprocrighttype => 'tsvector', amprocnum => '1',
-  amproc => 'gtsvector_consistent(internal,tsvector,int2,oid,internal)' },
+  amproc => 'gtsvector_consistent(internal,tsvector,int2,oid,internal,internal)' },
 { amprocfamily => 'gist/tsvector_ops', amproclefttype => 'tsvector',
   amprocrighttype => 'tsvector', amprocnum => '2',
   amproc => 'gtsvector_union' },
@@ -476,6 +476,9 @@
   amproc => 'gtsvector_picksplit' },
 { amprocfamily => 'gist/tsvector_ops', amproclefttype => 'tsvector',
   amprocrighttype => 'tsvector', amprocnum => '7', amproc => 'gtsvector_same' },
+{ amprocfamily => 'gist/tsvector_ops', amproclefttype => 'tsvector',
+  amprocrighttype => 'tsvector', amprocnum => '10',
+  amproc => 'gtsvector_options' },
 { amprocfamily => 'gist/tsquery_ops', amproclefttype => 'tsquery',
   amprocrighttype => 'tsquery', amprocnum => '1',
   amproc => 'gtsquery_consistent(internal,tsquery,int2,oid,internal)' },
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 87335248a0..82b51fc1bb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8569,30 +8569,35 @@
 
 { oid => '3648', descr => 'GiST tsvector support',
   proname => 'gtsvector_compress', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'gtsvector_compress' },
+  proargtypes => 'internal internal', prosrc => 'gtsvector_compress' },
 { oid => '3649', descr => 'GiST tsvector support',
   proname => 'gtsvector_decompress', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'gtsvector_decompress' },
+  proargtypes => 'internal internal', prosrc => 'gtsvector_decompress' },
 { oid => '3650', descr => 'GiST tsvector support',
   proname => 'gtsvector_picksplit', prorettype => 'internal',
-  proargtypes => 'internal internal', prosrc => 'gtsvector_picksplit' },
+  proargtypes => 'internal internal internal', prosrc => 'gtsvector_picksplit' },
 { oid => '3651', descr => 'GiST tsvector support',
   proname => 'gtsvector_union', prorettype => 'gtsvector',
-  proargtypes => 'internal internal', prosrc => 'gtsvector_union' },
+  proargtypes => 'internal internal internal', prosrc => 'gtsvector_union' },
 { oid => '3652', descr => 'GiST tsvector support',
   proname => 'gtsvector_same', prorettype => 'internal',
-  proargtypes => 'gtsvector gtsvector internal', prosrc => 'gtsvector_same' },
+  proargtypes => 'gtsvector gtsvector internal internal',
+  prosrc => 'gtsvector_same' },
 { oid => '3653', descr => 'GiST tsvector support',
   proname => 'gtsvector_penalty', prorettype => 'internal',
-  proargtypes => 'internal internal internal', prosrc => 'gtsvector_penalty' },
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'gtsvector_penalty' },
 { oid => '3654', descr => 'GiST tsvector support',
   proname => 'gtsvector_consistent', prorettype => 'bool',
-  proargtypes => 'internal tsvector int2 oid internal',
+  proargtypes => 'internal tsvector int2 oid internal internal',
   prosrc => 'gtsvector_consistent' },
 { oid => '3790', descr => 'GiST tsvector support (obsolete)',
   proname => 'gtsvector_consistent', prorettype => 'bool',
   proargtypes => 'internal gtsvector int4 oid internal',
   prosrc => 'gtsvector_consistent_oldsig' },
+{ oid => '3998', descr => 'GiST tsvector support',
+  proname => 'gtsvector_options', prorettype => 'internal',
+  proargtypes => 'internal bool', prosrc => 'gtsvector_options' },
 
 { oid => '3656', descr => 'GIN tsvector support',
   proname => 'gin_extract_tsvector', prorettype => 'internal',
diff --git a/src/test/regress/expected/tsearch.out b/src/test/regress/expected/tsearch.out
index 6f61acc1ed..a1873dc722 100644
--- a/src/test/regress/expected/tsearch.out
+++ b/src/test/regress/expected/tsearch.out
@@ -260,6 +260,182 @@ SELECT count(*) FROM test_tsvector WHERE a @@ '!no_such_lexeme';
    508
 (1 row)
 
+-- Test siglen parameter of GiST tsvector_ops
+CREATE INDEX wowidx1 ON test_tsvector USING gist (a tsvector_ops(foo=1));
+ERROR:  unrecognized parameter "foo"
+CREATE INDEX wowidx1 ON test_tsvector USING gist (a tsvector_ops(siglen=0));
+ERROR:  value 0 out of bounds for option "siglen"
+DETAIL:  Valid values are between "1" and "484".
+CREATE INDEX wowidx1 ON test_tsvector USING gist (a tsvector_ops(siglen=485));
+ERROR:  value 485 out of bounds for option "siglen"
+DETAIL:  Valid values are between "1" and "484".
+CREATE INDEX wowidx1 ON test_tsvector USING gist (a tsvector_ops(siglen=100,foo='bar'));
+ERROR:  unrecognized parameter "foo"
+CREATE INDEX wowidx1 ON test_tsvector USING gist (a tsvector_ops(siglen=100, siglen = 200));
+ERROR:  parameter "siglen" specified more than once
+CREATE INDEX wowidx2 ON test_tsvector USING gist (a tsvector_ops(siglen=1));
+\d test_tsvector
+            Table "public.test_tsvector"
+ Column |   Type   | Collation | Nullable | Default 
+--------+----------+-----------+----------+---------
+ t      | text     |           |          | 
+ a      | tsvector |           |          | 
+Indexes:
+    "wowidx" gist (a)
+    "wowidx2" gist (a tsvector_ops (siglen='1'))
+
+DROP INDEX wowidx;
+EXPLAIN (costs off) SELECT count(*) FROM test_tsvector WHERE a @@ 'wr|qh';
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Aggregate
+   ->  Bitmap Heap Scan on test_tsvector
+         Recheck Cond: (a @@ '''wr'' | ''qh'''::tsquery)
+         ->  Bitmap Index Scan on wowidx2
+               Index Cond: (a @@ '''wr'' | ''qh'''::tsquery)
+(5 rows)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'wr|qh';
+ count 
+-------
+   158
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'wr&qh';
+ count 
+-------
+    17
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'eq&yt';
+ count 
+-------
+     6
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'eq|yt';
+ count 
+-------
+    98
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ '(eq&yt)|(wr&qh)';
+ count 
+-------
+    23
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ '(eq|yt)&(wr|qh)';
+ count 
+-------
+    39
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'w:*|q:*';
+ count 
+-------
+   494
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ any ('{wr,qh}');
+ count 
+-------
+   158
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'no_such_lexeme';
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ '!no_such_lexeme';
+ count 
+-------
+   508
+(1 row)
+
+DROP INDEX wowidx2;
+CREATE INDEX wowidx ON test_tsvector USING gist (a DEFAULT(siglen=484));
+\d test_tsvector
+            Table "public.test_tsvector"
+ Column |   Type   | Collation | Nullable | Default 
+--------+----------+-----------+----------+---------
+ t      | text     |           |          | 
+ a      | tsvector |           |          | 
+Indexes:
+    "wowidx" gist (a tsvector_ops (siglen='484'))
+
+EXPLAIN (costs off) SELECT count(*) FROM test_tsvector WHERE a @@ 'wr|qh';
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Aggregate
+   ->  Bitmap Heap Scan on test_tsvector
+         Recheck Cond: (a @@ '''wr'' | ''qh'''::tsquery)
+         ->  Bitmap Index Scan on wowidx
+               Index Cond: (a @@ '''wr'' | ''qh'''::tsquery)
+(5 rows)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'wr|qh';
+ count 
+-------
+   158
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'wr&qh';
+ count 
+-------
+    17
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'eq&yt';
+ count 
+-------
+     6
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'eq|yt';
+ count 
+-------
+    98
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ '(eq&yt)|(wr&qh)';
+ count 
+-------
+    23
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ '(eq|yt)&(wr|qh)';
+ count 
+-------
+    39
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'w:*|q:*';
+ count 
+-------
+   494
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ any ('{wr,qh}');
+ count 
+-------
+   158
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'no_such_lexeme';
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM test_tsvector WHERE a @@ '!no_such_lexeme';
+ count 
+-------
+   508
+(1 row)
+
 RESET enable_seqscan;
 RESET enable_indexscan;
 RESET enable_bitmapscan;
diff --git a/src/test/regress/sql/tsearch.sql b/src/test/regress/sql/tsearch.sql
index 637bfb3012..9aed780d0c 100644
--- a/src/test/regress/sql/tsearch.sql
+++ b/src/test/regress/sql/tsearch.sql
@@ -87,6 +87,51 @@ SELECT count(*) FROM test_tsvector WHERE a @@ any ('{wr,qh}');
 SELECT count(*) FROM test_tsvector WHERE a @@ 'no_such_lexeme';
 SELECT count(*) FROM test_tsvector WHERE a @@ '!no_such_lexeme';
 
+-- Test siglen parameter of GiST tsvector_ops
+CREATE INDEX wowidx1 ON test_tsvector USING gist (a tsvector_ops(foo=1));
+CREATE INDEX wowidx1 ON test_tsvector USING gist (a tsvector_ops(siglen=0));
+CREATE INDEX wowidx1 ON test_tsvector USING gist (a tsvector_ops(siglen=485));
+CREATE INDEX wowidx1 ON test_tsvector USING gist (a tsvector_ops(siglen=100,foo='bar'));
+CREATE INDEX wowidx1 ON test_tsvector USING gist (a tsvector_ops(siglen=100, siglen = 200));
+
+CREATE INDEX wowidx2 ON test_tsvector USING gist (a tsvector_ops(siglen=1));
+
+\d test_tsvector
+
+DROP INDEX wowidx;
+
+EXPLAIN (costs off) SELECT count(*) FROM test_tsvector WHERE a @@ 'wr|qh';
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'wr|qh';
+SELECT count(*) FROM test_tsvector WHERE a @@ 'wr&qh';
+SELECT count(*) FROM test_tsvector WHERE a @@ 'eq&yt';
+SELECT count(*) FROM test_tsvector WHERE a @@ 'eq|yt';
+SELECT count(*) FROM test_tsvector WHERE a @@ '(eq&yt)|(wr&qh)';
+SELECT count(*) FROM test_tsvector WHERE a @@ '(eq|yt)&(wr|qh)';
+SELECT count(*) FROM test_tsvector WHERE a @@ 'w:*|q:*';
+SELECT count(*) FROM test_tsvector WHERE a @@ any ('{wr,qh}');
+SELECT count(*) FROM test_tsvector WHERE a @@ 'no_such_lexeme';
+SELECT count(*) FROM test_tsvector WHERE a @@ '!no_such_lexeme';
+
+DROP INDEX wowidx2;
+
+CREATE INDEX wowidx ON test_tsvector USING gist (a DEFAULT(siglen=484));
+
+\d test_tsvector
+
+EXPLAIN (costs off) SELECT count(*) FROM test_tsvector WHERE a @@ 'wr|qh';
+
+SELECT count(*) FROM test_tsvector WHERE a @@ 'wr|qh';
+SELECT count(*) FROM test_tsvector WHERE a @@ 'wr&qh';
+SELECT count(*) FROM test_tsvector WHERE a @@ 'eq&yt';
+SELECT count(*) FROM test_tsvector WHERE a @@ 'eq|yt';
+SELECT count(*) FROM test_tsvector WHERE a @@ '(eq&yt)|(wr&qh)';
+SELECT count(*) FROM test_tsvector WHERE a @@ '(eq|yt)&(wr|qh)';
+SELECT count(*) FROM test_tsvector WHERE a @@ 'w:*|q:*';
+SELECT count(*) FROM test_tsvector WHERE a @@ any ('{wr,qh}');
+SELECT count(*) FROM test_tsvector WHERE a @@ 'no_such_lexeme';
+SELECT count(*) FROM test_tsvector WHERE a @@ '!no_such_lexeme';
+
 RESET enable_seqscan;
 RESET enable_indexscan;
 RESET enable_bitmapscan;
-- 
2.20.1

0005-Add-opclass-parameters-to-BRIN-20190611.patchtext/plain; charset=us-asciiDownload
From 20a6e4eb0c0223feaebd996371a006698bfa1d01 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Mon, 10 Jun 2019 18:49:52 +0200
Subject: [PATCH 05/10] Add opclass parameters to BRIN

---
 src/backend/access/brin/brin.c          | 10 ++++++++++
 src/backend/access/brin/brin_validate.c | 16 ++++++++++------
 src/include/access/brin.h               |  2 +-
 src/include/access/brin_internal.h      |  3 +++
 src/include/catalog/pg_proc.dat         | 12 ++++++------
 5 files changed, 30 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index ae7b729edd..63f2d7990c 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -102,6 +102,7 @@ brinhandler(PG_FUNCTION_ARGS)
 	amroutine->amcanparallel = false;
 	amroutine->amcaninclude = false;
 	amroutine->amkeytype = InvalidOid;
+	amroutine->amopclassoptions = brinopclassoptions;
 
 	amroutine->ambuild = brinbuild;
 	amroutine->ambuildempty = brinbuildempty;
@@ -518,6 +519,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 											PointerGetDatum(bval),
 											PointerGetDatum(key));
 					addrange = DatumGetBool(add);
+
 					if (!addrange)
 						break;
 				}
@@ -845,6 +847,14 @@ brinoptions(Datum reloptions, bool validate)
 	return (bytea *) rdopts;
 }
 
+bytea *
+brinopclassoptions(Relation index, AttrNumber attnum, Datum attoptions,
+				   bool validate)
+{
+	return index_opclass_options_generic(index, attnum, BRIN_OPCLASSOPT_PROC,
+										 attoptions, validate);
+}
+
 /*
  * SQL-callable function to scan through an index and summarize all ranges
  * that are not currently summarized.
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 012933833b..4222a1781f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -93,18 +93,22 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_ADDVALUE:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											4, 4, INTERNALOID, INTERNALOID,
-											INTERNALOID, INTERNALOID);
+											4, 5, INTERNALOID, INTERNALOID,
+											INTERNALOID, INTERNALOID, INTERNALOID);
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INTERNALOID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INTERNALOID);
+				break;
+			case BRIN_OPCLASSOPT_PROC:
+				ok = check_amproc_signature(procform->amproc, INTERNALOID, false,
+											2, 2, INTERNALOID, BOOLOID);
 				break;
 			default:
 				/* Complain if it's not a valid optional proc number */
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 612721baf3..8271404332 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -37,6 +37,7 @@ typedef struct BrinStatsData
 
 
 #define BRIN_DEFAULT_PAGES_PER_RANGE	128
+
 #define BrinGetPagesPerRange(relation) \
 	((relation)->rd_options ? \
 	 ((BrinOptions *) (relation)->rd_options)->pagesPerRange : \
@@ -46,7 +47,6 @@ typedef struct BrinStatsData
 	 ((BrinOptions *) (relation)->rd_options)->autosummarize : \
 	  false)
 
-
 extern void brinGetStats(Relation index, BrinStatsData *stats);
 
 #endif							/* BRIN_H */
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index b1c9199946..0532094e28 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -68,6 +68,7 @@ typedef struct BrinDesc
 #define BRIN_PROCNUM_ADDVALUE		2
 #define BRIN_PROCNUM_CONSISTENT		3
 #define BRIN_PROCNUM_UNION			4
+#define BRIN_OPCLASSOPT_PROC		5	/* optional */
 #define BRIN_MANDATORY_NPROCS		4
 /* procedure numbers up to 10 are reserved for BRIN future expansion */
 #define BRIN_FIRST_OPTIONAL_PROCNUM 11
@@ -103,6 +104,8 @@ extern IndexBulkDeleteResult *brinbulkdelete(IndexVacuumInfo *info,
 extern IndexBulkDeleteResult *brinvacuumcleanup(IndexVacuumInfo *info,
 												IndexBulkDeleteResult *stats);
 extern bytea *brinoptions(Datum reloptions, bool validate);
+extern bytea *brinopclassoptions(Relation index, AttrNumber colno,
+								 Datum attoptions, bool validate);
 
 /* brin_validate.c */
 extern bool brinvalidate(Oid opclassoid);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 82b51fc1bb..bc3d08caec 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7887,15 +7887,15 @@
   proargtypes => 'internal', prosrc => 'brin_minmax_opcinfo' },
 { oid => '3384', descr => 'BRIN minmax support',
   proname => 'brin_minmax_add_value', prorettype => 'bool',
-  proargtypes => 'internal internal internal internal',
+  proargtypes => 'internal internal internal internal internal',
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal internal',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
-  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
+  proargtypes => 'internal internal internal internal', prosrc => 'brin_minmax_union' },
 
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
@@ -7903,15 +7903,15 @@
   proargtypes => 'internal', prosrc => 'brin_inclusion_opcinfo' },
 { oid => '4106', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_add_value', prorettype => 'bool',
-  proargtypes => 'internal internal internal internal',
+  proargtypes => 'internal internal internal internal internal',
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal internal',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
 # userlock replacements
-- 
2.20.1

0006-Pass-all-keys-to-BRIN-consistent-function-a-20190611.patchtext/plain; charset=us-asciiDownload
From be7b3bb9ce6c0ab3937c074cd5497599c8a42590 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:04:50 +0200
Subject: [PATCH 06/10] Pass all keys to BRIN consistent function at once

---
 src/backend/access/brin/brin.c           | 125 ++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 164 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 116 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 292 insertions(+), 121 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 63f2d7990c..31d09bc88e 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -383,6 +383,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -404,6 +407,53 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -464,7 +514,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -474,34 +524,19 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (!nkeys[attno - 1])
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
@@ -513,12 +548,42 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					 * the range as a whole, so break out of the loop as soon
 					 * as a false return value is obtained.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/* Check keys one by one */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
 
 					if (!addrange)
 						break;
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 86788024ef..f9c2918031 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -252,53 +254,103 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		matches;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = inclusion_consistent_key(bdesc, column, key, colloid);
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_BOOL(matches);
+}
+
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -318,49 +370,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -379,7 +431,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -400,9 +452,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -419,12 +471,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -454,9 +506,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -464,30 +516,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index ad0d18ed39..2266fef2f9 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -31,6 +31,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -147,47 +149,99 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
-	FmgrInfo   *finfo;
+	int			keyno;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = minmax_consistent_key(bdesc, column, key, colloid);
+
+		/* found non-matching key */
+		if (!matches)
+			break;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(matches);
+}
+
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -230,7 +284,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 4222a1781f..257096c615 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -98,8 +98,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 4, INTERNALOID, INTERNALOID,
-											INTERNALOID, INTERNALOID);
+											3, 5, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID, INTERNALOID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index bc3d08caec..3fb4073367 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7891,7 +7891,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal internal',
+  proargtypes => 'internal internal internal int4 internal',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -7907,7 +7907,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal internal',
+  proargtypes => 'internal internal internal int4 internal',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.20.1

0007-Move-IS-NOT-NULL-checks-to-bringetbitmap-20190611.patchtext/plain; charset=us-asciiDownload
From e040ce7299b2bf691b2c4ed4dcc682a754b4d8e8 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:05:39 +0200
Subject: [PATCH 07/10] Move IS NOT NULL checks to bringetbitmap

---
 src/backend/access/brin/brin.c           | 116 ++++++++++++++++++++---
 src/backend/access/brin/brin_inclusion.c |  62 +-----------
 src/backend/access/brin/brin_minmax.c    |  62 +-----------
 3 files changed, 109 insertions(+), 131 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 31d09bc88e..cfeb4a9c72 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -383,8 +383,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -409,10 +411,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -434,14 +439,12 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 							  keyattno - 1)->attcollation));
 
 		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -449,9 +452,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -538,6 +555,83 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys, and
+					 * if we're violating them. In that case we can terminate
+					 * early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					{
+						ScanKey	key = nullkeys[attno - 1][keyno];
+
+						Assert(key->sk_attno == bval->bv_attno);
+
+						/* interrupt the loop as soon as we find a mismatch */
+						if (!addrange)
+							break;
+
+						/* handle IS NULL/IS NOT NULL tests */
+						if (key->sk_flags & SK_ISNULL)
+						{
+							/* IS NULL scan key, but range has no NULLs */
+							if (key->sk_flags & SK_SEARCHNULL)
+							{
+								if (!bval->bv_allnulls && !bval->bv_hasnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * For IS NOT NULL, we can only skip ranges that are
+							 * known to have only nulls.
+							 */
+							if (key->sk_flags & SK_SEARCHNOTNULL)
+							{
+								if (bval->bv_allnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * Neither IS NULL nor IS NOT NULL was used; assume all
+							 * indexable operators are strict and thus return false
+							 * with NULL value in the scan key.
+							 */
+							addrange = false;
+						}
+					}
+
+					/*
+					 * If any of the IS [NOT] NULL keys failed, the page range as
+					 * a whole can't pass. So terminate the loop.
+					 */
+					if (!addrange)
+						break;
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed. If
+					 * there are no regular scan keys, we're done - the page range
+					 * matches. If there are regular keys, but the page range is
+					 * marked as 'all nulls' it can't possibly pass (we're assuming
+					 * the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index f9c2918031..b7b3f9937e 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -259,63 +259,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
 	bool		matches;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -327,9 +270,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = inclusion_consistent_key(bdesc, column, key, colloid);
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2266fef2f9..fc5e61b370 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -154,63 +154,6 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	matches = true;
 
@@ -218,9 +161,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = minmax_consistent_key(bdesc, column, key, colloid);
 
-- 
2.20.1

0008-Move-processing-of-NULLs-from-BRIN-support--20190611.patchtext/plain; charset=us-asciiDownload
From 9edb50e7a368f73da94e205b8ace83dc6a245fe1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:06:43 +0200
Subject: [PATCH 08/10] Move processing of NULLs from BRIN support functions

---
 src/backend/access/brin/brin.c           | 260 ++++++++++++++---------
 src/backend/access/brin/brin_inclusion.c |  44 +---
 src/backend/access/brin/brin_minmax.c    |  41 +---
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 169 insertions(+), 179 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index cfeb4a9c72..67f42ed5d1 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -33,6 +33,7 @@
 #include "storage/bufmgr.h"
 #include "storage/freespace.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -75,7 +76,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -172,7 +175,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -236,31 +238,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -353,6 +331,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -556,69 +535,31 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
-					 * First check if there are any IS [NOT] NULL scan keys, and
-					 * if we're violating them. In that case we can terminate
-					 * early, without invoking the support function.
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
 					 *
 					 * As there may be more keys, we can only detemine mismatch
 					 * within this loop.
 					 */
-					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
 					{
-						ScanKey	key = nullkeys[attno - 1][keyno];
-
-						Assert(key->sk_attno == bval->bv_attno);
-
-						/* interrupt the loop as soon as we find a mismatch */
-						if (!addrange)
-							break;
-
-						/* handle IS NULL/IS NOT NULL tests */
-						if (key->sk_flags & SK_ISNULL)
-						{
-							/* IS NULL scan key, but range has no NULLs */
-							if (key->sk_flags & SK_SEARCHNULL)
-							{
-								if (!bval->bv_allnulls && !bval->bv_hasnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * For IS NOT NULL, we can only skip ranges that are
-							 * known to have only nulls.
-							 */
-							if (key->sk_flags & SK_SEARCHNOTNULL)
-							{
-								if (bval->bv_allnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * Neither IS NULL nor IS NOT NULL was used; assume all
-							 * indexable operators are strict and thus return false
-							 * with NULL value in the scan key.
-							 */
-							addrange = false;
-						}
-					}
-
-					/*
-					 * If any of the IS [NOT] NULL keys failed, the page range as
-					 * a whole can't pass. So terminate the loop.
-					 */
-					if (!addrange)
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
 						break;
+					}
 
 					/*
-					 * So either there are no IS [NOT] NULL keys, or all passed. If
-					 * there are no regular scan keys, we're done - the page range
-					 * matches. If there are regular keys, but the page range is
-					 * marked as 'all nulls' it can't possibly pass (we're assuming
-					 * the operators are strict).
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
 					 */
 
 					/* No regular scan keys - page range as a whole passes. */
@@ -766,7 +707,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(&htup->t_self);
 
@@ -795,25 +735,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1614,6 +1537,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1667,3 +1623,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index b7b3f9937e..9cfd978507 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -504,37 +494,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index fc5e61b370..b8cff2e567 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -49,6 +49,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -70,7 +71,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -78,18 +79,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -246,34 +236,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 0532094e28..21a79803b3 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.20.1

#46Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Tomas Vondra (#45)
Re: WIP: BRIN multi-range indexes

On 2019-Jun-11, Tomas Vondra wrote:

Attached is this patch series, rebased on top of current master and the
opclass parameters patch [1]. I previously planned to keep those two
efforts separate for a while, but I decided to give it a try and the
breakage is fairly minor so I'll keep it this way - this patch has zero
chance of getting committed with the opclass parameters patch anyway.

Aside from rebase and changes due to adopting opclass parameters, the
patch is otherwise unchanged.

This patch series doesn't apply, but I'm leaving it alone since the
brokenness is the opclass part, for which I have pinged the other
thread.

Thanks,

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#47Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: Tomas Vondra (#45)
Re: WIP: BRIN multi-range indexes

Hi, Tomas!

I took a look at this patchset.

On Tue, Jun 11, 2019 at 8:31 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Attached is this patch series, rebased on top of current master and the
opclass parameters patch [1]. I previously planned to keep those two
efforts separate for a while, but I decided to give it a try and the
breakage is fairly minor so I'll keep it this way - this patch has zero
chance of getting committed with the opclass parameters patch anyway.

Great. You can notice, Nikita updated opclass parameters patchset
providing uniform way of passing opclass parameters for all index
access methods. We would appreciate if you share feedback on that.

Aside from rebase and changes due to adopting opclass parameters, the
patch is otherwise unchanged.

0001-0004 are just the opclass parameters patch series.

0005 adds opclass parameters to BRIN indexes (similarly to what the
preceding parts to for GIN/GiST indexes).

I see this patch change validation and catalog entries for addvalue,
consistent and union procs. However, I don't see additional argument
to be passed to those functions in this patch. 0009 adds argument to
addvalue. Regarding consistent and union, new argument seems not be
added in any patch. It's probably not so important if you're going to
rebase to current version of opclass parameters, because it provides
new way of passing opclass parameters to support functions.
------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#48Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alvaro Herrera (#46)
11 attachment(s)
Re: WIP: BRIN multi-range indexes

On Tue, Sep 03, 2019 at 06:05:04PM -0400, Alvaro Herrera wrote:

On 2019-Jun-11, Tomas Vondra wrote:

Attached is this patch series, rebased on top of current master and the
opclass parameters patch [1]. I previously planned to keep those two
efforts separate for a while, but I decided to give it a try and the
breakage is fairly minor so I'll keep it this way - this patch has zero
chance of getting committed with the opclass parameters patch anyway.

Aside from rebase and changes due to adopting opclass parameters, the
patch is otherwise unchanged.

This patch series doesn't apply, but I'm leaving it alone since the
brokenness is the opclass part, for which I have pinged the other
thread.

Attached is an updated version of this patch series, rebased on top of
the opclass parameter patches, shared by Nikita a couple of days ago.
There's one extra fixup patch, addressing a bug in those patches.

Firstly, while I have some comments on the opclass parameters patches
(shared in the other thread), I think that patch series is moving in the
right direction. After rebase the code is somewhat simpler and easier to
read, which is good. I'm sure there's some more work needed on the APIs
and so on, but I'm optimistic about that.

The rest of this patch series (0007-0011) is mostly unchanged. I've
fixed a couple of bugs and added some comments (particularly to the
bloom opclass), but that's about it.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Introduce-opclass-parameters.patch.gzapplication/gzipDownload
��}]0001-Introduce-opclass-parameters.patch�\ysI��>E�#���%��a$l+#��3��qMw��nh�������������,��y�1GueUV���x��K��m�����h79��f=]���cc;�{T�j�]m���+6�k���z�����z#��y�F�'���+k�����NW����O?�m�[8�];v����/5��`�
/�F�Zl����z���h�R�U�����G�{/������kVo��l�������k��\��5G[r�;n>_�T�L���9��,�^��g�2
������?�_��������������+���[�/iTD�d�J������p�m
M������%:��i�'�2j�����c��e���g3�L��K{}[��3����O���$��v�;�K�������K�p�����K:Y3���*^�eT�@�k�`����y��� ��k ��G�k�f���-���&w��E'�&����L:��\�\-���_#�&��2�@����	���a	i�����@�n$�Tt�6�xF�,9��mp����|��DY�d�?����B��>�m�%�.����E�������;��gZ����@�; ��_�.�� ��(+���6�.A
	��mF���[���l,N�bC��5Z��-���5�f�J�]���~�k�{���)�Q��t��I2��������|�p�%]��#�Ss�[��H�W����z����;N�c�&��@Lfw�xw�l��jK��zZw�*2�Dj/a�v�L:�W����R{dd�t�cSd����dB��}=���)��dY����$�]o��p�pL�P�k��8D3R�)X(wL�
H��%����Lf�����;{�L]mezw>U����Ob��O%M�	�F��7�y���d����UF�������J�����JBd�V�,�.,��6.�,$�UH��:��w�~������� =�J�T���&3��EI����|�*���1��%g�my��1~<3�����V����uf3���z����|ky�������������v�� �P�6
5�M��g�\��]�r�C�����l�Y�`�� a�\�k.V��]vXCB�������ts�YV�T55�-_��r��rv����?O��N���������z��?�3�9~"���J?� � �������u�ZR1�JG�%>O}����p4i_/�l�Y./�&�_^F�|�w���O��������Iew������U��4��G<���6��������V\P�C�xp5��0Os�c/o��0t���zp3`��+��\{�+����.�!EY�>��WL���s�
m@�����;������/��5w4�v�����<e���a�im�+f��^�x���w+{���y�rq3�O����o������� 
>������%�z�B�r�N!RC���!�#^E��u�����dF�8��!�2s�X[��t��Y!��4u���>�@!��Z`#hb�[\�6'���_y����BO���f�m\n��=#��v�0�[���Ye�%�*L��/3��\�p$]\�'�)���+�}Z��[p#}�c�G%�$����j�y��Z�6�����mw� Izc)��^���Dd�W��V,M��y��8�%�ZC��A�s���|qh&��={7����DX�{vq=�xws3M�������K6����o��d�����g�*��4��.�^����=��V���p�j=���F��|-����"6����1�K!��sPA��I�WW(��,X�����"M�
$��/`�x���wWWc�����x ����<�+�K�$���t���^��]M����P0���;��$�����&`�G*�2m���������Qd+�oI���p�zw�.����?N0��I�����m�b����k��6�f{G���[��H���������e�����y^�|�,0w��T�!;��dmJ���:!Tb#~�G%�%R_)���d���~7U6�(=����b��=
�vk����(�
����QD��@��-�/j��j�������X�����!F]�����d�@�����)y6&�������G%��LJ���D���7C0G�e&L?T6�����r�\�����4��������O���2������{c�<5��f��+��]^����5�� �"��,AJ����~����{4��wl�oM�u���V����U����=n�^76��E�z�[�����b�	�����7���{�������enF�+Z����3B���{n���*����f}��FKoU�-�h���V���R��4���h'�6-w��T\���L�������n]�=��v�Rk���E!���E������#�5uVF�bD�i�~����7�F����9z(uj�����_:���8��0}1�\��iS�i�h��^�SG�4���,H�|�W��(E�_qnLi�x���]���=�	8�5���=f��f���%t�p��a�E�����|��BA��:_����g�.p�Y��������ll�I|��'X�r.��a��W'~�6���l��-ZHm�'����B�`<�}����OC��e���s'���L:��kP���"b�����k�M!��"V��!����D���\�00-�,Z��"�0�o`�d~eL0,S�&�C�{��
	�t�������5�D
 LOb	�5��{^��b������������g�QA������b���dPh\4���t�k���>�3�>r�KC��	_�C��������
�/�U���vQ���Y�+�����n�UnpJ�V�A��j�Oe�C@j������qv-�JGC1Op���\���A���-C���}���swaCv��Sn0�{y@Ng�Y�8c;c7�+H���������UD�~L��Q���d~�B~��v��
�d�S��"I_ZZs�8)`?|(`.��+�
�u!��>~1D_�r�C&��c�UH�Va���6\�2��0�T|!����$�0d�q�R������C���T����[e�we���M�D�
�p(���v'��\�S�-p)�c���2{�2���5����J@�R��7+ACv���`��o�m)���(���QM����P�S2�dw����1p�\4O�$��@D�Wd��49g�yZ|����1c��EXx��C")|�ma�R�SE���Uj!k�ID��f�=�.K����, �"�����m��K���qe�J��?���������(�jH�8��~�Q�0�0�-VHb%���]���v��Q�����@�lBr�G`�	��������p�">���v�j�]�JQ��i;\F����Tu��x����-[�2�t�r�&UG�(���aop� 
Q�3_j�{
�F��`\�m;�Qd!�RPw3z��X�l��������0R�@`�� �{-IJ)��v�Q�O	,�5:�f�����r�,�=���h,N��6��@�����~r��~�?��{.����Bn60����s"~�/u=c�+�4����G���������+\����{��1j���p��Zh��26��@E�~Q��\���[.�/i����(&M�[����5�Z��1l�1�Z�L�)n���n���-�TM,�m3T���Kq<�`�%���S���"�Yc�?>e�\.��v8R�6%Z��^��#�������N��d%������X���4����iY�ZX��h�ScH�v[KG�f��/�<�/��V�8*��M�~��M��v0On��W��M@���/C}������P-��b�Fp�1�������t]��c�N��*��G�B��gY���,���D�j��;��q�R��,�C`����<��+��F�i���&��C��'�����������V�dG��O�KO�KO�K��?�j���]�;v�bb/UTLp�L;��0[�I;�[���OdP�.������,��m!��a�f>5+���UkV��T���F.�q��/����m�Nm��� �8������� zd������E���G�p�;j�!����OP��S:`U���}C<��B�W���]��Uw�t��	��.m�Bo��{7t�+^sm=��f�
���/� [,^������3D����k��q��p�y�%���eqem�����FG���1�]��@�Mp_�������W����-("���W��1����Y�cS%�0������)�U�W���	���4�m6v`	��pOt���*�TEp/��������|0�T��������h/�2@�K���?����7?����@�En����Y����;D�Q0����\s�[
B�#p��m���=�I��(����������{��O_$cAK�?~������������3I��9S�x/�>`�vpv����J_��^9���l��-�E�'����W��y��2�Wl��H{1z?����&��#;?I��S���=�~_+����	��|�bL �n*��H�[�f
����Z#w��~��G��k��DB�[����������t8��5�������&��)|7(�.KwQx�Y9�e�L��P2������?{Vfn�(xB�	y�e�<��x�LCB(\R��,1�g{�t�Lu=w8��B�"��Pg����xBI��H}X!���D�X` \t����]s8[����XEU�	��"�@p�7��;��V��k#��pv;���4�J������L���W69�����]:�Y��@�����!L�?������$���<����e��v$��qC
�JZ����($��lk��eD
���m�pW�$H�$;b��<�&��4#�&��
���^�?��P�"�U!��,������C(�d��J.%������:pVG�]+*�^	{A�u�������=7]q�O��������|`t�����"V�J'���������������8:&���Q�1�P!S�
�I^�
�yt.@�R��BP�t@��|�����}yBwB;������B�39�
���-8T�W\4u��]��������lmmpE�NL8]�S�x�]^-��B�����!����)�8��O0��"��I��Y"�fz�u��+�%4�CP��'z�~aG�`�Y�`?+����8J�n2$$c�� !�{A�C�M���o�����8� �rJ�z@���������{���i(U���� '���n��8������s)�����\z�[�="�EA��]���@�\%5�C(�����n��% U8uC��R����D�2����0�����B�R��h�Y����P�VC�6�!�*Jv�l�+�!�������Z�qC���'jz
����4Z�f;|<="A���?�����,��E�`$2�P~�r�P� 4uV�,e�I ��)��W��,�������j0
5Xd%�8QH����y�?	���q�|'K��<�����,0��j;��!(�M��Dx�V��I����r�A.�b]y%2��������f�����Y��\F�B��X�Uu���V3�m����z�5%����"(��r�T0��/-u��xN�b�,��1���L���1�Q��Yp�CF�O�y�JD.�>��`�D�C6�T����$i����>]1�����Ui�2q,.���\��J_dx~m#���}�D0��j�����9���O�!
��&��T�����L��e�����j��~Z3�7oj�^��Z=���;�w^�M6�y�6YU��w�o�u�u��_�����hzq��������f�5��������e%*JC1�zC;�i�����n�y�m�a(���J�r��U���2������>Mo1?___eYL��:��� %��������*K��~�������N�Z�u;u�U�k�
&��L�I��E�#4�p��B�����x�]�o.���o�n�H��[��&�H��	����e���-y%9���e�)�!H������^�h
���������$��~TWWWW����v�g���fm�_1�`o	v��x,M�Ly�w���2)��h0���vv��8
�^���7�C��RZ�gd�rT���P�����8{R���q���%	A�5zo�������e!=	��=�l^�fK"���%~��or)Yt�lc-�������!_Z���������c��������C��z<^3r�5qZ��XFa�'�\S
0���������\(��L�!��YV���cS�����m��h�F7�
�"k���"z�{�&��<9|�D����U���b�]�d�:O��H��?��l
�������/�2O6�WQ[i
Cn�#���VX��q�x����|Y���3P�RV���z����i�i�d����t6��{H�C�CcV|����R{�Ls������<nB�Q�^E+�����jX�.N�;�FX�(���w6ii#3U�gq�W�5._=��������U������-LE�Q�RE��������#�]�7�Hxj���:����D&:���������;pXA�s��5=diN��]|#"j%��	����c����s0�������hD���|F���(����U��������pGl8(�=����6!J���+�;�[����
��x�|�#Y�
'�3��;^G��F%mo"��!2����X��fHayN��^|���Oj>e�p���4� ��0�e����[4y/a9��������,(L�4����+x�2�����b��!�%���h=���r��(@T��J�� E���������a�E����U<��0�8�{*��4��%�����k4w��K�����Q���Z?D�<*-���9���Jj�>�Y�x�2+���6.����E�od�&��.9A
�>j��yy��e<���gES)v=�IA��p�����+;�W�3���t��W ��rY�W�@^H�{5��L��V���R��*S�M��7c����P���U���j��.UlKeG��V_��iEK��ZB�����uB!�����rZ<��S��J5kue��z�J��7m��L2�F��x�������l95Rcg����k6;� 
�����x���Xz���� 5�+���__\�����Z�"6#+
�E��d���^�:S��B)��p��n�o6��x<��z���R�5�R�5�-4���_�X��*����W'��^*I&�5��3[o\�(�1���0��I�v����
Zh��J������'m�J��]��c��#>k��#rtr�`{8;����&-�{s*��ZE%��#�a��!���e��JE@�t�+9����	8D�J$��"���|�$�����{���f��6\�Q���3y��F�t�'l]�2��s���F��8���M�{������l��=���^U��c�T_�|Aai����u���u����C�;}r�,�a1F	��t�n�;g�x�/��
?5���J^�z���V��5Q1��K��a������������hPf��s���.��������<������=N)���vC���H����������|.|�A���Nb=�mEn�KV���YL�+�����@f�:�\t�#���f��C*7��c86i7;

��<��!��s�;\���d�����|���N�v�4m���t���X��M�M�tk�DY#T
������n]�d�S���-N����>�*�\;r�N����f3�'��?�*n�����r*�t��Q�GS��;�b��z��?���w�I�$���#��MaZ}�������D���G�x�X�z?�n���au-�	 =~�
Q��~��������('�����WJn~���1�E��M*24J&�g��w����jf���8��w�?|
��h�J�33��C���_�
h�;:��lXt{���a8����S�{��Y�O
��*���w"W+� y;�"�/H�]�%��t�j�Ib�n90�E���}�Z��#�7����V7U�e���L}�9���.#��kEA��tTa���J��n6�^;�������@�{�;���Hb���l4iC�yF�@Z�#T����
Ui%3#t@�)���2_xp�Ut���#Js��S^��),��-��f�^����"�9���<{������D�BH�����h�.��>�����V]�,�0�K+1���.X�p���E�/r��^��9J�+.�6xr����l5�d�l� �)C�f��Ct��Q)5=�!���14�G���_���U����{Pt4�������������b���+�E63,4C��D�
��9����n�
�ug�7�
�N>���laMVk:�'ZI\���h�t���}H����J����z�@j*|��!�D2�wH��K|����u<
��c������m�@�~��� ����d��������d^{��x���R,��%���OpJ��4�2G�z���{�~�����~��������Ek#�v�����b���r�HTM����{)�B�B��F���q�*�d\,��6�,���<�"O��w�S�J�{7�]G�A=�|��0`C��/)�z�^���������;�;���!��EH�����n�;��V/*��hPA��,�a�F{_���axHN����?�K�	�j`�.#�#�G����+�j�~X�(K�rzG��*F��Y�J�m���_�(�r��P��l~���M
�q�P�3�F�
j,����	E��4'n��I�j�y"X���p��d�*E�+��l����9�ao�pZ#L9����3]
?
M���gw�9{�B�4�O
j��>�#�������-Z������Q���VknsU|�����7��
��v���;|���*R�)�C�D�(0F.������*��W�����m�=S��6:�vS�=�V9,,�k�IE��
"�#�O���=�,#�
�������������
�����s�O#$u�$�Q:`D�-na?b?�n�Q��5�{�%����
�*�3C���P%����w-�?��0��Q�e�/��Ns���d�$�W�n���e�	���^1�|�@P�r�S��Sv��>+@%��J���&�H������)A�JL��c���Qk�x��:�~%�R��z����b����'?^'�����j�/�� �!������@G���2������#m�[7j�����Od��F�a�����X(U�N��%�_m��F[&�Z����*M%�
��?^�@+��a$�
�Qe������>�(��U�|P�sg2 g{����s����h�.	��;lx�z&C��Pkv��>	���
EG�L����Nh�
x�zA'�
�f�n1�A��V�
M����J/�T+3C�GTx��{��7�-��4��hzZ�'j]>��,��T#������V�a�c\:�G������
*�aN��+j�f�1���G����ji:�I��n��x�5�dk�Nz[����*$�lF�pymTG�'<zM��]'��vBG0�u��]9��+]?_�����M��������@@�1���R������8�>}3z���M5�K�m��)�!��n9���y���������Je�)�K6gb��q������szr=zq�����_OiS;��L-[�
���=�e�W��z�)�f�K���p�$�b�V���1z��u���[tAW�����nq��xy���ojfj	�F���	�=����:}���� ����/Y����
Y���O^�|z���p����%�s-34/�s����Ef��C��`=���C�������S����0����baR8�g�2�l=�\�����8�Bl�Ad�e7l��6`������0�e�C�j�V�������{U�{U��J��0���i5�%A6l�^QL��P���R8�z��[
���"��1�f���P-�����U�n���1��/��\���j��k�9[�
�2�S�Ry�I��g�&Ucd���{��M�[�9���!���^�LB�x��?����V��v[jY�7�����f=�����[;�F-����l8:��F�H���^�D��s���ap9U0��t���P��@���m����������]N���C��,�!1�o�d��)|������*��d���60�kM�TjkSl�Z�`^�����i-���4${�������qy����r��o�}�e;{�X`��������+:��i���z�l�Ym��T����b��!��<�S>��W���e��FP|�Vh�c��asT
�a�D-������>����������z�@��4�$Ow�7:�ud�
������b�-������SS�5�������������HHOkzJr��������b"��G�M�'8#����v��I����MMz��U�jX��8���H������(D^�~��OH�PC�>���O���5�$RF�5���
�2u#u�/<�@T�T������o�a������5�14���Jy�FC0
�d�(w9�$�����2�N2�������g���,;mo<zn�l���$�D�����XOqn�y��d �'�I;�,�,A������G����N.��E�c�<;}������T��,���9�im]LU.^8%i�pM^~r���R�@�S`%�d�<23m�����?��63������i���f'���7���M����l�2�h��]��O�9�S1u�
�a<�\2�����\3Z�����dc^�!Sk�q��M!.��k�����z~��
�M�;�q���Msk�S6�f� � 'LO�@R��e�g�V@�$��2K6��h�_mw0��7��P^ �����A.��h���.`�.!k/a�d�a���U����u�a��L`Ku'������7��QM��Y������y�r���1hi�$0����Zq��tyv�i�r��[H6�kY�^��������Bm8W s>�{/���Ue?����X:�Hr��E�����x1�g��*�qY6��`��{n���k
��I��rS+��8��Yi��C�z�`�������pF�-��E���h9��N���5�f���l��J��o�y�A��[4�{�`	��|+����ZX]���gw����q��@�#%����Yj�g����������p�1/���
����]S
?.�)��^>�Y�����tf��t6��}�nE�fk8�������<��Q$�l:�����?R�sX�p������e����Y\�&P
+�������+S.Lt���7t��#%?�����PL#���� �OI]hO���������m��e�����8�W
�jG��T��e4�����������C6��it�}���f���a���M����9.j'��������VH�8n�k�~�%���Lig���d*P��*J6����4[j��.�8��yf���I0�C	�08j���wb[q�kz����f=�aM�	F'���Z
�����k7}��M|���
����H�k(�6���/0��uuw�!�O71f�������p��U�d�^��<~���_z����[��Q&"�}�2�fYQ��&'�o�'R�)�9E�'�u�,d9�A�^r�<C�y�{�Q�K����'&��9��O�f���|��GW�bT��d~��D�J��	m���W+4i�6������QS�����|z:a<F-�bc��M�����$��#?5m!=���J[�+M���g����y�%�4�JJ�h���w���F��v��4���?{/����}��t!Z'A�;;	��F{������_��8���]G�ca��#�)�n���G��GXd�M5��cPp�h�L�>)��{����MDn�A��n��������l��'"������N����#j�u�8k��d�^�(?72.�|M��b9�b��U,d6�I�4�
���K#{i����l�K�aK;?(T�#��zf3i����5���S��99V*�]}J%W����2����U�(��D|���s��E��|v�YN7W���9�������(�����|)��G���4��c��?�SU���s7_��8���12�*���Cv
���#]�X�.#�y�i��q����������x�ASor�p���c�T5�q��+|!7!YNv��
����� ��z�	L�E!p#��^7���C�M�nr9�Ox����,%�t�k�[Y��3�a~q�}��W�l��:�����W�j������k�	?�kw�t�����v+�63����X3�"�Z���d�Z��bt��H����6=������u:|��������������j��g���r`r����N�H�~�2p����5�F*�u��V8p����V'��G��U�E��Hs�#
'~\�0|y@��sS�=^O�q@l�t�BK�A���r�9�[��1�H���|��E9�O�0�\d"H���j�q��V��w�m��O�!${T�v>����em����S�O$���3go�����8�g�.@Hn��Ft���2��q+�*}�S�@6k\D��;�o��Wn�l�[|.1�2B�y��E�G�B�*8�oaNn�2���\�1e���	<�F�������t�D�Z9J�Bu����6gM'���u��BTzQ�&�-`���v<��j���az�m�2�v�c�k	�j��\�2c���Xfd�
R's��9glq�����%���(����4]d��[Zk{NmOi	r��3������;��/��h���e�]��	o�>���IM��Fj]�Tj�J���*�����rwK@#��cD����{���sg���b���r�B�r5w�8YL9t7��j�����xq�B�C^�G�j�&�N	9<�"�8��w���9!�P�@3o���8�gt��E���v�_c,�71��h�Su�����&�����M�1C�b��O�8�?��4u��:����HPX��4��E�d���	��L��
�����g=&�*�����:�xl7��C���h�ff�����h���3���
Z�}Ui�R�7��� �S1�wJk��	y>��������qI-F����6���p���VTEk�]���jc��U'�T�*����5K���i���C�����v�r���>� #e�K��O|��M�����jh:9{O���8q�������x����4���� ,�..O��4�l,A�u?7�SU�\13����Gfa����9&T�S�������g�
���w����w����%��$O �;���J������E=?Ag3��ST�ci��T�&�g����#�Y�4[�Z���/&��n���S0/1��:iC�.��DD������G	�~��
�B��VP>�
����ng�J`$�����|�!A$����8��{���W��G�T����`}�p9b��	�U�~����>F������Q��:U����X'���^�z�*�6�g��A�8��y��(��m#Ap�������+��+��\n�g�1V8
��q����%�)g�a$�8��x-���-d ��*O9����(#g��{�cc��;.��JYh��1��Y���]��u�*&Oo����r�|>S�}����3���S�����&�^�y8�?�%~��=E9���w
�pG_�Xx>=/a�:�9��LV����u��7*��Z���~����K5_����/���/���/���T�&�n�l����$g�	r� �z/8��zx�����_OuTa���C�����(c���r��`2n6{�A�����"����*����0�=6����FK�������R/+��u+J�+����.
p� Bfo��U<�8a� �|��f�$��4���d*Q�D�~	's4@&�j�\�o
�;T[���;x=k�{��UF�6��;�i*�ES?:�x�����n��3�BC��j����d�F��d���B����e����3T1�,�P�+*"�1:"?�L}���|R�T�B)������*�|����15If:x]~��o�D�+	�&�`4o����G��83�}:5�D=�)Q?��D�������SR�53%�-3��4SvJKU�����o�4�o��}uz|y���
����`)v{$��S*��!�T��~��M��0h6�SN�Q{��0R(���Os2����	���t�|����sR ���-z���nQT(��{�	D�Q������|���0��B
�*�C����)��f�������wt�!5���� ��>��'��H�'Jov`�k���8WDpj��������7���h��Ui�����
�5�b��O.�u>sny��7!*�� �C�6�7[�X�l��a���6���"~�q`�����S��!�d����Z�!��w�C�����[?"����D	,�8�*^=�r��������?�����O
hg������G�!��P�L�O��nDQ7��?���n��p�-����IY�i���#\�����b�� V��\VG���[�P��y��Dx�f3i]P�@��&���:�Q���'��hVZ�s�6�8���������p~������_�g��1������e�J�t[Y}G�m�^{��9���� ,PK(?%�D^��GO\w�#��`�Z�:*b�C���Ma����� �iT��+J��8�b��F�m�x���B���.���O	
��z<Qip	3`����c�\�����B��E�;����3g3TJ���A��#
����+�!�G�$}�����K�}��b�7�j�E�wv��
5�o����^(_J�R����rr'v=��P���
1������A�'6��f!q}{�e*4O�>�����O;J&M*�����I0�i�����7���<���L�8vz�K���<(���y�$~jB&�WxIJ��4�'*/}�_���
�]��E��{���\��f�'#��\,!�$Z��5����>���g{k9����
9�f�S!Ep���h���A�sx�������=����B�8����u,�yb�]b5�!�h�Z�b+:	c�����R�p����
x�z�/5��%��:d�~p�M	�W���K8'n�vm�6�������S����q������*��0����*��A�$C��7
C��<
�b���V
5��"���f���p)��kXT��&�I�#3/���_I�%H�3���g�^����������|>:�g��Ss�x�V6�43��u�{��eh��u�������rr`,`;�&�X�sp�G.H.���T8��*�N`v=��!�Iu�����Wh��t��U�4�x��m�9�%2!B���3����Q���������%��n��C���du�����m�/��##���
g��T
����t�*�V����?"���e�����m63���av�,�t�kg�l����xr���������D
'KM4m���5��%v��F��0Wx�2����c[�������]�2�k�$���u�E��b�T�l��q/���` ���S��G���>
�-r�M��/�%�L3��NI��2eB���5���F���Wb��lXn����P������V ��
+6d^h	@72+��bz_��b:���J��d#����~7�����������D�)�c���x���y]X0mS.���FW�<��M��/��k<�/0-��h�F8���z����h�TQ#4
��>�[KP��b�[p2��3Z	RN��N�w���Y#-~h�1���PX����[��F��Z����T8e��:q�0���F�41�F�`k�N#X��~;��-oQ��f<�N���a:������T%dLm���,��B��Nqh�a���p�nk����t��������.����_"JH8�Y����RL���KFy�w��]�������J7i��P��-7���UjIU����a��=u���Z��@A��8A����������9�9�ej���q+=ehC�����=��\��,��$���N�O�`zz[���Vs���H��)G���8G��q��8�!�.��t��l������1	!)Q��4�dYovq�"Q������R����%R�K����U���ZU�da���HJ�j x8nww��K4�-��>���>m���e}�7�Y���~d�����n����GB����(c� �� �X��������iD�VCz���-���=������HWh��A���A���\�����/��z����<$��9��e=�(,e.�
���M�������s�
��7R�^#W6�k���W<�aC���7�\�m�~x�r��ccfKq*�}iI]� ��=��`�C�{�0�
mO�RL'l��_�~�^,�@����[��c���|-
�����p��Si��V'X���=�����6{6}�s�r-�����\���8��AB�V[tME�Ca���5�1xt��r����;���z���+j���':}CF�Cln�L�4#[)�*'�
����1u�S���������������~w*��Q��x�4vh�;F����N���!��D���\>L�����ngG]]-�������8�C��h����J�vM)0qY����D3���^��=Q���^���}�;��n?}��|I%)�.�������n`�"��EK4M@F]
�a|7
c`d%��W�?{�XExq���q��z<F�������\$t�<�j���(,�6�9BFH����CM���,e=|ia����%F9��h�O��sKQ��\��d �G>�o������T��^����:a�ziy��d�j��%SS)�dr���Q
v����f����d#�CL��<"��k����n+������������g�/F�/N�_�(G��F����o�
������������k���)��N���m
�����C����TK��?erPB�f�!3���EV{��j$��\i��tv���]��
�����
~���L��H�d2������ }�H��0��`�������>D�D�Nv�������c/#�vAM#�jg�H�P�G:;���Kw����0nd�C�N������(Le[v��D<������tGw`>��v�'���v���)���B����:�=���>����E���3�gz��1N4=��)@�k��7����0�d_���$�`9O`�@l_u	T�1���8In��j��ii	�y�Vc����1���z��aZp��^)�q�J@@&�������|Bq2v5fh��u�*��I��T�|�fl�������>�����6NX����;c{�L�}���.���hiT��k�)�)�������+��oL����3
�
�{�9ofvPp��&X�_��b����4��t��E?����

����b�D�+>I�-V������PX����cY����9w.{���^�<s�5��)}gy���)�ix��hX����2�cAx!�TL��Y�H~��,BD��b���b2Q�	���Z��&}���pM�-B�x.������~�����:F���!���e��2�e�����)~�. �����A��W������������e���Ff�z��{_RUW$�4��i�
FtX��RBzR��~�2�f���F�+u����^e���^uy��N��F��g<���%��7���4���Z�c+�#��SZ�����1��4�hrX8f�4EA�
�a�m6�^4������4���x�e ���|uX���a���F@�e$M�M��?$�M����$3�4�%1��>������v�p��}J���LU'�a����.�	WyZ�4�Lo�
fm���s�m�K'�����^�w{����7�;�o�l�5�O���\����s����'����/^�t.�[���!u�����3_gd}X��C<���Q����*��f����Uj�l�J�Y[���}5:~�U����dQBW�Y����r�G�^� Bto�����XvR�8��@��6]����Xhr���/�H�J�YFdz�3���|�P�������:�>�|5z������������3>_ "��Z������754����#��*��_V���+'�r�[�����'�(�����0�s<�g�y��Xq����.yJ������_`��o��V��O(����4u�#:��^�7$�Q���+�a��z��q$�lmr��(oWK��p{�46���
�!�%��������LT%kE�UTA/��MnK��U��#[����s�P�;����?�DM�Gau�T�Ub!~��V`�t��;l��t�������Y~��Sd�<������r;���=��R��v��g>A�'��;kz���gl��
�G~#V��n��2��;�xwM��$b=�`}�8��|��w
K���pU�Nu�xOC)�|9������E��������*
����S���k<0�k����T��������
��lf@A�<f�$��@��l�YDY��bq��#g�
�������s�F������D��U��G�	����[!'3��9�j%�d)N<w2���K1p=��v��b������Da�Z��$J��s��hJ����xP�@�#^EOL�]'U���0;�
�f���KK�����^��H#�Zj8(�@Zd#bg	?���:j����&_�<~q
2�5�6:Y8yZ,/�.���pq�)��t�� ���j���J*l��^�wQ����A��{�����SN[i$�A��V�C��S��` ��`L��B��qL�h����KRL^�o��rN�d�e}�����Q-���;@	P���<�������������`�y�+����0����h=2�J3�J'^)I�)�?c��i�:N
��Z��\g`c0��r
|�p�t��`�d�+W?0�8��9��3�+b:��+!l0�����������Iw<�'��7���^7p+���������`
&� E��z(��������,�2�������z���C��X���T�r"�T.�(k�D�I���
������,��y�;NPbY��3�� ����Q�,��8�������
�q{���-N�]/F�0����j(#6Ne����{=�6�t
�U0y�iA��y)��|����P�p
BT�����W�V)z�Y����<� �~8-�0>�|Y7,��J���h�����Z��>����{*V~=;~C.7�{��?�F��� �*F�&��r��A���[�Y��7�z��%�yi5�cr)���.k/�"s2C��<}MX;��SM���4]����2��y����dXMD����_���R%�.Yh�
����#��2���w?D��e�����K�[r�^������2��y�b�<��[�b-�����%���
�kT��m�V3,d���/���� FI�H^��)��4]�>lw?�&�q�y�n�����F�7�)�z#i���6�=j[���\��s���I�-��p(��|a�<Z2�n( ����)���T�����\-�HFk�a���D�r�=8W�4��m[���x����3e""��G�BUqtr���w�O�]���*�%cs`E�6PM����
(��q�DT��}r
z���������^��E*������-A�m#T�}�����K����������������8Pff�q�]����#���i���Gr��������b�f�d��^e�6H����V����� �����(�9.mf���������T�Zy�&�k�I�:�1�A7-�k���Y���l��w�'���8t^p�(���H���n�h�G:_�t��o{��������.���+�c�k	�\�{P$��V���g��Dk������X������_#?��?�;|rE�������es�R��i�:��Se���� ���S�*�y�o"j��,���Y:�qx�|��j5P����[������[5�H$G:u�}�q���;�����������Q�\��G4v-�Yw�y���&��,����Z�{�z8��	����
����
��>t�jh���0�R��=8�<=�>-}�0���V��Z�7\d���fCpEYTE1�0���$�g�i��N�����
[������=�F�iYR�P1�P��Y��B��".��o1G�I�y�����-�Z9����W�� �tC&��]�K���r8U��+��\�]���Qk��������p��,�����$����9��h����������7N48���)���B�}��5ZBah���1r��QS
c���s�5��Ho>_51X�_a��6J�of%�������`��`Ta��\�x��u���N/������`������� ��p�����7i�Axg%5D
lS�y;��
���p@��
^w��������&�
M&V�)�4afu����d>��]q�+V91%�Y���z��M=�%[��|�%�,^�m$�lV���������N�mt}���VUn"�\v��V��%��������p�h#<'����������uAN������*��rVIrH#}lj���@ �qw�W�E�I�n����n��� T�����dFu3��`����e��8����0��u�>���Z����g13G�l�^=f��f�#����� ��ju	����n��F~��q���N�M����*�P����tm��""�-��p�����3�_9����0^�I��O/^_�{�����o8��*������W��q'����k2��6�zgg�1e"~�e�l��s
i�s�v�y��M��PD����w�8���%�!��MW|��7P!p�����"
�iG�0�����?����Y|��Uu	!n*Ff������{oJ4�����?�:7���(S���$���(�9r���?��\�f�^>5��=Q){�^�������l�a��=�e��/�����l��BQ?�������H
������6���o/@�;��-�]�!p
��������n�tQ��p+�$Tl9��M|	}�z�_�9�6<���5�������-��[n��Z�s���d���I$��?�U�+��'�A
���o\W��J���}�:�7�F�������?]��OV������d��=�=4�b��&!?��?[�|����D5�;*qw����r�>
����(q�7�}RI.��$�A�O:���5�}o�
���b����
�$����H�����zu��B�!
���g��*�/�K���~4�'�f3����i
�M�)��*H/������@>��,S27�C�����lu��Fd��`��3@�����kZT[
#D��V�������txGye���G�I�9
�M�y��@�y��T��5=��������L
0002-Introduce-amattoptions.patch.gzapplication/gzipDownload
0003-Use-amattoptions-in-contrib-bloom.patch.gzapplication/gzipDownload
0004-Use-opclass-parameters-in-GiST-tsvector_ops.patch.gzapplication/gzipDownload
0005-Remove-pg_index.indoption.patch.gzapplication/gzipDownload
0006-fix-amvalidate.patch.gzapplication/gzipDownload
0007-Pass-all-keys-to-BRIN-consistent-function-at-once.patch.gzapplication/gzipDownload
0008-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch.gzapplication/gzipDownload
0009-Move-processing-of-NULLs-from-BRIN-support-functions.patch.gzapplication/gzipDownload
0010-BRIN-bloom-indexes.patch.gzapplication/gzipDownload
��}]0010-BRIN-bloom-indexes.patch�\{w�6������n#�zQ/�����8��4v&vv�gv�DB'��eu���{I�Tl%i]�Mb������7H�I�����o �����T������C����������xc�6
�����/:�g���v:�� �L\Es��E��Hq�������s&�B���h~��Z���x��
���Z��
��Yo���/�:@q�2�K��3��w'W�~N��8�/����q�~���ww�����"���3��q��-�M���Ut������Y����D{#�]���=G�=��Gzm��JkZ��V~T?P�u������)�����r����Y���t��
2O���f��HKo��KU� ��3��3\�� ����H��������3d1~bR�Dny������M{*M�b7�Z����C��)9��e11~bS����n��H�NA'�	�M�����+�e)�h0�`����H��O�f.0�k���X&2T0��LyY�V8eP��V�/o��N�����n�cR���3��Z�3N����nM�(���:\�
n�w���G�����{Y������~�o�|w��'�lN�T��g2�pc����d�=tg�k����Ig��u���R)���7S��;�t��
�3��&\����gB$��v&&Q��1A����s���HD�n2�Oh��{7OD,a��=�w	��v-�w��t��g>���[�����T�&�����"�cp,,U�G}�����<pmZ�������4X?T4�D������2����������f���JB�`��D�$�_�'���i��$�*�2�w8^�Jn� Nz����b�<t�ng��5���='�b�s$x����c��p#�b��%�����@�'=y�>�j�	:�G�����������������*v	�n�`������$���I�ec���D���m<����a��y��7 H���T�+B��*�P��P9(p�-�U�l������`�����A����.U��J`�������R���>���J�2�q��!�\�Ul�\	���0�KWz��i���0 Z���p�!����#�E���Nk��q��f�c�����#�MC9����9q�-tU�l\	�a�fs����������5~�L�\��G����Q��m�Y��x[��Y&����a
�������y��
f	��� ��#S�_	3jfl]�%�������U1L���v�0O�J��-pz.��!��[C��e���/�?�����K����2��0���BW�.��w�N���6<�y�5vv�X��E�+��(|��������+w�j������N�2��4h�F��;��Q�?��>N�?�?�E����>u���	������z?�p�c��;��,����9�#q�-�U�"l���!��A�p��&B�I���%�q�g�$b��,t	4W������/V	 9���Z��|��������H,`X��f���g�I
��d*���d�Kjk����JI����$d��V�q�����3?�6���#�}<8O#�i$����f��'���%J#QD�T��t���Q��|A�R=� �g�$ 7(3�)���MH^Uk��o��l�Xy�>n��� X�\��*��V0E��W��(r����XY���������9J�4P���b�����y������.,���Y�����4��$�O�L��������j��6uA�T�
��.-u���v�����E�3�����������w��V?j��7rz?��_������2�5Y�}Kn��A���S��x�B��5�q6�Q�x!7��8AtT"��Wc7�����\����.7�p�b�w6��S6��
�N}L�|�q1�;�������p�N�\x0�\+���T�,>L�J���h���~��V��oS	KQ6����[9�����\�aqS?��q������Y�>�;��C�B��L3��D0EK����%y�U��`��[�fI������PW=��b& _F�G}�B���3`kA��A�M �5��&2�tJ����,��	���!~�����X���K�"�
|��k}��������d��:��3�
��V�3w�.}�m�jbofT�4h������c����������p~����������Op���������_���p����q~q%N:����kG���*���������MC�Ts����Lv�d�l�ki$��t8�����Q�]�
i��Y,��K�,^��*�^���9Bm����"�(l�d.��t��T�GC��h`����RIP�p9?F�?j�O�/�{��/�f�Yb�yL�v�B����FM��~�Y�
W����=�!���k���)q�<=��Ph�"q�I�����T�=%�o�T6�jo���0�$��	Ij,���C5����b����L� ���*f����<No:
�S0�xy(��O0��q���l^$��4��50���1�����Bc����-qE�f"� �UI86;-�@��n���O���Q('�-�JfP6��/1���Y�%���Y��4E,"]h$�����;n���ld�����Q��]���
Z�E����1�w�	�:�E���6�y��Gs�[�d�0h�!�X���;��9���T���@y�VK�����R��v')_w�w�(v��X�G����O��j������K+��VU�e,��4�����]q��?/����?#l�D��u=���D��~�	�)��?�M�?t�3;&Ob���UhE�}��4Q~����e�h�a+W�k�@j��~�����f�V��
��^�-��j����`��;�����������6G�O*��R��F��������^�[��be\��9��n��`�7/q�M;M$�E���x�����RQs��9<6�g�#4�
s��I��A
��2�����
�^M���>�C� ���+�k`,��e��pQaY�
Ftm�F4�9��:.{��D��AD�h!�d�C&������p�m�)��I�P���P��n��������L�!��XMP�Y�-6�S
��}7%�O�0O�3%cL�,{�����:]����Tre�W���[�D�m��dh��[T��H�b���*�f�t ��4�Qn���c?_B�4lR�G�����)ml�=<�T��,�(�
�$@E��c�����L�i�Ea����
S�������W��IFs�C������1���lj^V��I.�Z�\D�G�������e
��
�N��e���r���Z�[�!{D`������_�e��bF)g�.!\K�BJ�&~jE�xD*����&jJ��fS����zXQl���F���,�4����B'����$b�T��q�����n2��h
��Ij�P�-1� R5��j�0������=���bf��^!\�Ar	�4�=j'������Em����%��V���	�%H��'��D@�1���+A��o�es#Q=�z=2�P�l)�k���
���������-��P���`����V^���HSc�d%I\L`�Y�U��������6a��@��B���bE^�aO���K���^l���K����EX�)5�f���K�A�D*���JT���F�����?���N��e��$��Qg��*;�"���I}��E���%��z]���q�*/1`?p���'�' �j9c�<��r������m�� 8W�A,VIi�U�Pl�vX��RA�^ajh�22q������r�(�-dH�G���5G�.C8��w��*O5u�L�
Hz�@�+���X���V�U��P#�^���:(h�
f1HS���p<�o�g�Y8Fc3i"=�� �����,"QZ�����r�����H�2j�6*D�Ut�c�GA�x6�)�8z��^���(�P������H���,�r����cEI���`��[/j\���d��P>�b?��B��:����2����d	*��20r���%��[�p%Kfoyl���a�7����Pvi"�9E9�>�����u�G���{pQ?�Y��[hP������>EA>���
b�8r^Ep�Z���{���L�������8��Mq��8�*M�e!��Cji�
^�umQ�Pmb��Wf�Se�"����u!A{0Y`gj�K�
%���M���TI�������AL����}Y�T�H�A$=w��m6u��Ds�q4�?���1t
Z���ZC��L��J�9W�����2L�yG@����P��=�0[�E(��� ���������c2"W�+9�d�<�n�6�h���Pa���W���8��Ai�s��%P��u��;�6�.n�6��Op��V��Ha�'<�Y�P�CZj�9<����X����a�&BY��$ ����>�JJ�y�>w�B�����9r������I�� ����������f�ap7A���rj��
���SR���#/���4�i��6-��#R���A;[���@� �T���G�(�N5����2$�F{����G
�}W�M�$
D�Ef
El��7���n��
[��XU��r'�4�2*6���
�
�[.nE�d��8/���*](��.�y���v�Y�^�����*`KG�P��r1�MQ�����q��n��"?.�hX�:Fk��Y��mUy;�-�Hc�@IUxv��
E��-W*�O+��6.�+,c�`�y��]��<�"����(�*%����*�g���W1/t�e����kQ�2���gc�t3�V�UD�X��,�l>�@}�]$��Vt��F�+��DW_J-xa&c�9Dp�D<v1�`S(P!��)'���	��N'�i6�������Is��-�J���62h���
���&�����0���j���0f_<07��{NpK�)�w��Tyj�V� ��|*�j���p��#;)�
$��������AJ���k���������yB�1qPZ���4�/{R�lXP��m���Dw��
�NK�^�08{���a�L<���yYrB�@0�4��P�+�(mG�k�_/�E{��pn"�$��0M�`<�\�e��Ok�T���d�����t�������������O���}I%m����F�'17�uk������T�rN7o���p�t��]����I�}j�U��Y#O���zD�
wjjT����x����oX�F�������w��iS����,��tkI��%dc�/�wa7��W&�nA��I�_=:=���)6���R�d��������Qo�x�����>Qqr��':#5�Sk +�>q�!�*�<�vVq.S(K��(�����������'?����xu���������6?�/?�J�5Wh���N.���4?
��R���)F\��M�[��0��9����~!�^�����J���^�����9{\Q,N�k�N������e����)�[��M ���t�4(����X�r�E�22H}<���,�S����%����������l�f��i�I�$K�LsA!sOG��'�=/
p3��A��}�o�����t�i�O������'���t���M��{����Kq��6���������%����U$./�_Y�M�^�<O�8�P{�u3H��1�N����cE�4����`����������jy�_�5�#�`,����A���jf�kH������,��0������^�l�0p��}����m�~��\z����������h��s���=�A;��{|v+/"'�F���/uZ`��ng'|m��w*y�	�s$���3�U2���3��{�����/�����$�oew�K�*�4`�������o�o���[�l�&�j��F�N�]'q��mj��N��G��4J��%W�$�i�g�����,���:��-
�@,�g�+^�@��'|iM��8ZB�
�F�rWhW�D��E�������	����Q�1�����������k�p����p��t~+��V��gKp��
	2h�|#�
�#�E��B����S������'��%�Vq����*�}J�\��{��U��.i��@�= M�U��ON���N..ON_^��;>o��~�%o�i����Z,�+�UC�S����Z���\BdT+�,���N�,
�6�����Z��)Q�Hua8)�;�W����������`1XvAak�D��c�+�L����l�!��
���z0c�!y}������.N.O�?��\ Z
!�\yM�-�Ro�If�i�P���k���������cV,����.1z��G���Y�����,�����Bd�|�y��M���o8��|�q:��!��GRX�ku�_����8��J4y]qK5OH���1��0��#'�R`�[���p�lEy��>9�[���A&rl�!K��x��%�@K���jL)��R�F�3�>?%��*�M��	1�l����]�rM�CT1�`��W��($�]*�R�`M���|HU"�@[��D������>e�*�c��#����q�5�"�?��>pWUa�i����qJ�����r�R��I�|6��A8��Nr3�0����|Ot�D�=��uba�p�����ig���L>���0)	��;#8���\���!�b�.��_�wh����[2�n�)�I�t\}>2�������T`��e����T6����CD�/�����,q\8�����`�"#!B)���M�Xb�`���t�5��W��/?��#�N�(q
�"3�)FnB+�$�
�P�(��p0yP��@���X?���t�����A��F�r���������0����������W��E��>||H���HMyR�#uh�����)F���%4=��;rxD;��@��3����B����\\��+9k����2�8�2s�V��F
YE�(������>�1��-[�	B���D��>W���������`(!8q����S��;�8��h��
	�����F����}�����U�X�+W�����u�b'�J$��t��tms�{��-rR��pE��(���ykt����J
M�r�X�C������'/������~j{�����_������L!(�k~����2�����-4���HH5DGHcOPS�c� 6z�G3��Z�?nB6F��M�\��%~�-N��c���	(H����J�	�����E� �S�
�2bXdD��\�>�=��(rn`�a�(=�Ij)�����B�����Z0�-0&�MK��\(\���tM|X�����������97V��E��-��g��R"�(\�C�M����������TbO���U�*F��2~��%9.x_�sN�+�n����D��X�b�l�l�E�Z�' �{Q��
o�O��9�f��%<�#o����
d�*���@��@0|�0��*�?@8'�������i��
Q��%���H.GhPX����h�_�d�z����@B��T��
��������v�]�L#�>�m|#bJ�U�����x
R������c���sq��s�8����r���Ys�H��\�����VS���X���a��C��K���������|�+��]�;.�/���_���qQj���V�$%E4��HI�������������u�u5�
��`�{u���EP��;���q��;B��7}=�aV�S��O���aG��>+b�Ch�,�yU�wT�y�n�S�m�/����h���Q(�����7W����[���4�q��/��TY�Nt���?�)����E�k�kJ����(c�0,�vbT�S�qv�p*���
f���3�V��9�`)���8�o��/m-��(�b����|��1��GZ"��1g*$�?Ic&�L���Oc�Z,��M�P0�����X���im9��O8�B� r�����(��I���^��F� �\'\�V�&?t1�}�P�,��K�AB8�/(�j#��:��f����2g*�@��C���}Y�I�g*N?@NS�,V�5�e�����G�0(��R����� �zJ��!��k`��Gn��9�"Ql�����w�EvY���G"3��."��.R�WD5Cv����)�dX�
���Wg�[F��HH�T3�W����d"�r��-JE1,]%|{"88�h,����+�8�Ol�Ecj@���6�h�;S������qb�}FF6�0b���y�xd�	�����,���a��l������a�W�P�C��g��lA56����]�������#����p�3������g)�-+W��!�z~
��=k���X����d���)����;P�-#��Z���%�<��x���Bl��c��dhi|	q��fo��sA��,����	��g�L�
�5��Cw174���!�}H!u\�`���>`�M���C
9��;i�]]��~�c��:.���r�)��
�J�<�g8���CQ�5%�`��Q=�H%��v�FP2Bs%_~�G9= ����A��?�\/(,�o3K(
��a���sB�]
���*r��X�Q
"����B`3Ge��5%�N�i�~������^��
��l�>���������#{�q��Wn�M����L��N'���%#��[r��U�3�_#���6��QG`$��`m��dLM����\����mV
�t�����@�N��<��;/��N�D�)�6�@#�"H�Y�d�!�F��������U���W1�C@1
��b0i����cK�1����q�3�1�q�:��g�Tb-�F���/R�v%�x�E��
��PM-$��F
o�g���)>6U���tM�������}�/����������`�`�f��L	s�@@���+
��v!K��Hs��
�!�������'L>�8�������Jk��Xo�e�*��qphT��%��^A�7��d4k��0�� ~�zQ�G��3�����5G?��q�8���"U�JJ��wK���L�=�������B_?����,��t+q�T����4���%��c���Wz�����_�cV�|�T��g0t�dHLO����Om��d��g���&���������!���`���!��F��g;��e��ek�N=�������P��E#����X�v��l���(������a�.p�s�hz��� �	����J�{)��>/��|�>�(k$1�!���;���f�����D-5�\�R��t�Wo����:��hC����a������^�|.���IY�(F���a$8�6GR<&I�c&J1H�+3m&�+���N��y�������!��D�������x
�mETt39��Z�26����}���$��fes_�{����W�K������;2�q�nu�Ag,�t~�xz���qY�+��F0lIbJ�,�I:��U�4�9�Qv����)4��\���������U����d*[�1��>�-;�k	���qDQ��0��U��j���xhx���iCjm�cr4��0���[0Z
�H8d����s�r�Z�"�LEL�o����2�&���������$��DcG��;� �GF�<P��G"��z4�Q�h��������yQ*�@p�=L P��!�UC�]
�h,8���b`%U~�{#��8:�������)?Z�y}�nW!0�
m����g7G���������&h���O3�E�W�@"�|y�hx�L��a��������h,]P��"�J�����;�[���o����������7b�3n�`_O��|8��1���|*2�4��a0������)����#�3��|dv��H
Pd@@

�����3��+A�
z��]��B_�Y�;��J�|������
2������TB9����|sZ��=�@L�Z�n&���E�*�y>�G"VG�O��oC��awM�J[�x_��W0!��<48�m��:54_��c�s��SbLm��ED�����2/8��_�=?mwvrzy|^�����@Y:E�rLfV�<I���DA�[�^��$���l��`��� K�������n�(�L�.�
����T�NNE����(D�*�,�#� ����lL��Q��Q���J�A��;��T
��S.��UV�P�($��L������U�T�����z"�Q���0����:@	�m���u���VK������8a��K_0oY*�J.6���<����������]�Xe���t�lqfd>��+�MO����< ����[TCy�D��������f-�3�Be��)�4t?���7��I��,N�F*�v��(����T�1���P�N4��aVI������=��6�lC;���[Qf"�/+�~��~���H��;�+��r���ua�y���M(�4�eTY�����{���*�PN����Ei�t��za�^�%�����Sq��I"�e@q}�L|b*%��jM
��V�@���z��?l6&�;:�a��"�1�����;-O�Q<�d����XLi�U� 8�"�Fcy��|d��b+�_���������$�td#_�$���W�e����U���jnR���6*
AV	La02��ih9_(L�\pZ'�=gi��1�u��-�� �/� ��@����REB�r�����9��)U���e
�27vnD�����
�DD^�H��OD�X���33��2y�lS���g��0k/qI��{�*�K�������q�h��3��`*��Ls[�p��qo�������	�]�F��	������x�o�/y�RX6��^������������^Z����:�|�m�j��5�������W��g��������������<�k%�`�x���U�(�m�}��
���\qn|p�*��Q"��~y��
��V���s�����|�
�COH��(���l6eL��K�A�b���#�v�w��?��H�LC�Z�P��c����@�L;kB�-���g��G�+�<�rpc4���	�p���r
�NN1�!��9��H��B@�����d�!<�h5}7��.\�![1�M
��6."UTr�����������K��>�R;���F�+�V6
}K->U�[��2�XF����!m����,/�@795�Y�F:'�����C��	z1_�^rW_��Y�7��X��D�P��Z�k�8���ee�X�R������_���)��7�?��>h_N����g�pX�+^����Q��8HU�V���O_tf�xk���{��������D<�>��w�+m:'�Hn���������Bx8T��o����N���b'�(��Y�nN�/}X@����c�	p��n�a�;����>t�����O>c�P��p�K"��������p��#|BWR�1��L���]�������'���|��-�*]��}rA�R�]rO�-�
�|H?7	��"�G�B�O�S&4�B��Dd�TS����(�����c�}����*�N�A��~Q�����6��%����C5�>*�M�C����j�+;QPU����B������2�qF1�Uz����AC����Fx�$��K
�F��S3O ����������$_H��Q�a�9L��XY���O�����H5�#�=���m��\k�Z�#�'*��<�x���4��F����D%\���MW�����|���0�\8M'�RO�SY�Y
2
������uW,�����vb�-`W���q���:G��t�U�O�G�#6!��l�tIm��hlo�8�������5*��vz���?��
8����������RG~��}et�Z��S	4,�����V%������EQf�)��=��h[��fM�y��JxL���*��P5������m2yg��'�����bC!%S����p�	����x@���|<u�4���:[��	|�l�[T�VB!��������{���P�brr������2�a���Dh��X�����B;�1�:]5����|�0[(#�!��W�S���G����3�@D�xKGa�Y�����t�m���y�"���+��|vJ�$)�������3H�
����w3Ho��"����#2uN��9;���Qe�C�HY�@��:�����h���;Ux����H�����O���v�.����E4�#�ma�j�����W��"���MO�yW���0ui�QNES��+_���%[rH��H4��g:�	l�<^`"�"�1IG����E��Y2���PS�PU�af�_{���f�������ht�EB�+������\I��atX����	f���?��y�F���%����)�PeG@D:�C~��>�4zx5u�EBCQg������W��x��N1���y��!��)~I���4|9�Xa�G8O�������B�18`/}�Z�C��?�-V�gCDx����E��!c6�8t�P|j��mE�TI�H�n%s��L�H����r�R�_�I����J�����@#�$�uf�|t�����^�Ac�YG�QPb����)����}��?������x
��H�e��>��v�
%}8� |���||�Os��SK���ui��]���}5
��S��h[��T�e�3Ts������.�\�,��?��L�{��{���Z���5���_�%�:W�K��c�������(�k���^��
l��8*��V����BV�8���Nu#����+}�?Ll<�K�s���� L�� �^�_m5[{���n���4��^�s{���29�l��h+���J�@�������Q*CX�d`�������D,U/���+�����z~�q`��&�����`�����&;�e �y�3D�NV_���mO;���r�����9��nFK
*T�Op��i���������E-CE����K�V�K�^#��k��g0�o$��|�������]���zM��s�B��97� /~��O�G4���~~^��e�Y�*�����q����b��u�K�6�j���Wp_�l�*b*�8L�|�LX�j
��4��a�9��E��v��x������2>4�����g���1�e��u����{�zuC�p�k�"ir�E�d�V��K���9��I�\� *<[@Th"*=�CThY���{�&5jf5�
���+�Q )�&��3^�����
vN��N
7;�]E���2���&I���^�^����!oY/C�������DN+�,bV^���w�-��d� ��r�F_��r��7
k��Fw��,X��kt'��k~����k����a����;��,X��k=��m\���-���IH��gy4hYFg��:�H=��e�W�[���|['�&@UDM&If�$���C��$�R���G�[��������������72m���.-���Q���1�IGB���9rh�wY�i8[H�Y����4�
�3���� �J=*G� 
��q�W���e�����>DN|�MPl���i�R[����l����Lx����j��o�N��
�2�m�������.\[Z����E/Z��z���X�$���^-KNg�����\�/��%�q�2�H	V"%��'^�.��6K�w��x��Z@�V>�P8��tTT������$	� �[7����Z@�V.:�"��(�o:�)	��������BR���Y��Z��GIQ�VA�W�{Y�����2^���wn��vRT��"���|���1Oyh�Z�����?����G�m�g�\�w�S�2��OehwgIPv���m�����Yxn�����_%��N���x��C��jy��O��)�WI~h���D�V5�V��c+���i+���������sN\��Iv���	@.\r������k���o��9� 7�`�����Z�uZ`�{w6���I����b$�SYHr�� �E�����T����ov0L~�%���m]�������b>��V��vU�#o�Y�.���]�w.���������\���\�
~���� �
�_d��2�������c<�:�������YX���3��N�{���<����e���#���p�q���Yt�%Q:��D�C�����������r�o��A%-���^�/����&)��J�9~�_��^��j�O���[(
��@>������wA3�?Z@w�"Dt|����a
+KZ��
�u3�)7�_%�A7�����w��/Z�4�xA��4���w�/1���y�
�-�44	�����5�,�M�����%}����^�oM#jF�d�W`�K���9�������x�4D\l���n�e���y��]�'�z��yfr��=��������W~�R�U�i6]%�~�������B~�v����y�y1Uy����G9-����e�@�/�����6"��Q����W�G�����~TiD�����j���0�^%����x�&����|����zD6<
���X����Q��/5H���aHm����U�����-�����(6������eA���})�|hT��R`U��
���l���3�K �3S^|P��^(��Hu��^�(�H����I�MA���3�`�p. �yZ��Vpl�mC��E�m�����`Z]�m��=
�S�|������t/<���ff��5����� YV���� ��i��-���D��[�(�hke�m%Q�!�Ve�b~�(������B�E�g+,UI��"�- XT
�O��"�,o�j�F+S�-L=�he)Fe���D�&��j�os��Cn���������X�K�
��0���)�jE����.W������\�;^���wG�sY��ru.���3Z���gBT���sY��vu.���e�����,Xw�:�^��Y�H��Kw�3�]�T���`��1�~���'�)H��t&y��4���@�j�mC��C�m����
���@�L�K�M1�G�a�`�p�r	pF�6Y! �.C��\�(����%]-S�m�e��*��p��R�G��m �C�m��H�M��X�@������G�A�`����!%fYRb��<��	+��r������U"R��{��IF�F|�!��8rC���I��!�6	�Z�&�g�Cvm��e�F��2@���\A��pU]���]��]���w�+vy�n�.�]��[,���rt����@E#�TM#\��Oz!��A9��j�R��
Gh�[hV����/ U�x�r�_M�m�uH�M��i�i W�h��ou������l�yv-���j^!�3�f��U^�V��x����7��x;���(����l�mBS� �(�6	�:%�&�\���$�k�w];�"�U�t�3���v�f�3XU��+�{$���FAo��C�����]��)nk+�Bo���Co��DDo����7���7��
�q�+�����VI�_EIE��
�QqEu.U��7wo�Fg�v��� �!�v���%�v��'�v����m�5d�ntu�u5o�ZV�R��"��f�RjU��'���
��Qsa��M��t�VF� ���WY)=A������������V���Lk������6|
H����
� T��;
Y$5C��*�^����@�"�(C�KC\Q��]�w!�rP��%�,�Vzw�:D����x'���!�	�����YE�2R�M)�>(u5���X��������e��m����[@�������+`�wAT%�����pw��o����[�����A��n��fA��������,w�L�,wA�*s�����U���l_up�[X2vY�L,<M�K�/���D�����
���,�
����r �'�x�k�]<��^O�p�o���^K���V��D_�n���W�E_`�[�`�4�P3i�1�6
��<�pm~��A����(p�q[onu�M/�>�tSwQ���F��i�G��(o�H-���I��U�t�w��X �F��+
��y��+#�
bF��a^C���!\K��a\O,���\U�m�5�Vmz�����}Z��;Y|��neq`5�eqU������Zka=�M��lD���%�����,���d��[���hk�x�m%���`Z]�m��S*����2�#�����6}%��u)�W�EE�7k�r��>+�l��C��{/r&o_?���e�F�[���(�k�[�pU5m���.�6�:��!��h�.�I�����J�%\�T\��^�yn���yO��Wm��][�^?�[�����&�n���������2��l�l�W�5*��\
W)W�=�l�V�K���%l��������C
��.�v��z.��Cr�z"�2'D{?2���vl�K��|8���������'�3��D}�O
Bp����r���������F�sSHY��"6��2�������,F�sw�Y���[#j��A��nF}GB
F]�</@������E����
����,
�,Ze��P�P�����PV������BY=���*(sj�P�����S�r�y�Y�/���Y������.��SE����@SEs��I��� <(R�����a����"�C3��<`P
�<�P��XTq6���Jk��ZL9�U$�|�T��)H����:�^m�����P���U��[��W�+�;o�.T�jW
V����-F��PP.��<��H����"A^p�lh�X���l-F����#+(DI���[�#��$z���qD��
��������������gn����zq���q�����LA�R�=����y7�;��ps�0y����NcPy�6�u~���,a����C�p:���G�f�Cvd�SCm E���0|�a�b�yC����H��+qe��	s�|Z���2LG���� x�@i*^uH���!����kG���2�.��<*n*D�W�M�Hosf������K�W�@������g���+�j>6�`�����%Y3Wz>0��q5NLGq���t�gJ��\e����x�@�T��~��b����r�S*W�GE�4MMN��y�H�% ���{b�BpyTD��3�Pu4f�&hG��?��I�Oze�n��$���9���������S�k�����HNG�0��xaqq+�rn����6��csl@�����a^���{bzZ� SQ.\�Q^��Q�z�0��UA
����/�2�M?&��x�:O&7e���x������L��V"x�^M��^�����{5��~���<`�� z�h�q\{�r����wK�q�V�	"���x�)#9r@������}�����A�d$1X�)On��4���5r��M���9K�
�S��9����3P��s wSr���;=Gs���N0�C�`���g�8��Z9��h�('hi���y���� �]�N��K���J��@�^���E���/ ]^fO�]�X��	�}�����g.@�@{��9�U����sK�2�@�eK�9����2�{v��Z��v���Y���s��,��q+�y�U~��;���sL�N4�����]��y�V�������~�
T��~����Wj�\O���.�h�|E.hyR�[���+�\��;V5�Xi���U^�J���B���g@����]���b^.u,5w���'�;5V�R~�c�A�w�&[����^���w���j'�5��^���S#e_�^{��$��o�@����w�����(2��(o�_��i /�A������hR���S�K��	~������z<'��=�H'��
&I�0�H��C��~���4SVVW��8w����$`�,=Sqro���V���3�����f�&�S��2����"kV���Y�6W[�{�@��B����t��mlq�%a���k>M&�q��$����\q�LMY�Z�jwf�`���(#>&,�}L���(�b
v��M-����0�
�b���u<���dH;�O@�����$y7I��'����;KzO��v�z'w����W�^"e4�������k��z\o��(�����OF�����^�R?Y��@a��������]�xs��D���"�������bP/�����C����o���e���)����������	o|^!�
~�g�����O�_L�x���W������$�`�������Ozm2��&������o�L�3~#o����m�/��������Y�e<�����������vS��J���r���Qx��P�'|j|��3W����P-�N.����C������J���������Kvrzy�����7�//�X|='��2c�n^-�V��@��t��O����@��|tp��������c_3��S�7��xn��=��)���>���b<��mV������G�I���b�q�wp@l����|1��������b����R90~rq2��'��|����|LF��xW��:��l�Z��vk��'Q}����2��l�yB,q�df>��~c����6�# �lU�Q��V�����������e0�c)��0v=����D5V�T�}�%�0��n������Vj1�@{�N����e�#���Og��M��L���_�P�������V���b�V�H��D�^A.�����]��Y��X��,�*%���7*�n`�'9��N��;����Q@B5�2�z�-�[����U�4��x"P�WS�WV���Kq����[��C����:>g/~b��#�����K�`���;���d���M���O��y�5e�T��'>6Cmw�)��b;)3������u�OZ���fR�����~������.�<���T�O/2�q��PKu������H����f�����C��=?d;\4*=��������A��@��i1o/NN����"����M���-_��N&m*�o�����A?>??;?`D
|�`��Yg���x��C�pl�:�<:y��>r����I�'8��������"<���=�n5�{��]DP?N���x:�����E�D����Tq��!��!���6�}�<c�CNT��j��'�_�gx�o0mS����i��~"�p;����L��V^������D����ZaOgD��{:�������j|
SB�w�h�>�$Ok���������mR��y:R���� ���~o?75}�����G���Q���%SK>Wa�9���7�����8����km���?�R�T~�{}�=_l|��#r}�����'���/��Mq|]��7��1Hq��^��T��1��?z���,�-�4���2����!�zqz�����:V��B��[�=�%d�o�V��T���n2�?���za�z
9=e����U�x����o���D����������lev�F��j�����z����3Iix�H	NM�?/%����GM�M�U���CJ�ck�������Q�5��H{��F]�����6t�*�)6H<�D��Z��jUn�{I�;�;B�D�����U&4&$Q�����?�������(Wif�x?x������J�G���S�v*U�
(-S�R�D���^���2�*�&���f}�R�����5��e�����D%������*���*���o��V$d�J�H����
��5nV���-/�CI}����q�������;��yO�C�lU���Z}�5+��~����{��j�R��{@�.#���Q�;��?������:�.������]j�\Ro�Het��O=j�E�2xS��\�zu����/`<�|st~������wd|gj|����l�
3���_���4�M��n3P
���0������C.���/��99�^���	W��C�	}���	*�B��y�tC���_2�����2��F��Q�D'�������7'�?��|{sv��U(�����5����	$�
�g'��d�4a �3���1��@�:8d�v�M��7���]5��.MJ)��\g�qp�.��8�����;y�TMg2�T0������;���,:�����%KFqg������n</�����`v_��U�Ii�,u�T�$��=����[n������/�?~��H��F���wE`��&�a������c@
�^F���
H��7'�8f[�^ ���	�q`C!@k�G[���Tf�9�D#����=��"�}�(�G'�����S�l�=6���d��u�"tF_�# �K?k�a�F�Z���3�A&�f2I�O�$���
�!Q��xB�t�W���M��T����a6��L�g0I���l
�I�s��d�n�����L�5E��� A������^o?{���=~�7������@`B.�gcw�3D�lLs��d���F���������{X$@A�
����W��,�'sh����X#[��[������2(��q�QKB�����%o  ; �#�����3�d��z�3�oN��.5�J����e���:\[��#���D��9��5��R3yF���H-y
V>������r-��<?v	��Zc������p��?W@�}4�����h����A����*0Z��E��U<����Gq�:�����@k�������b�M��J�3|�����>��18gO�	��b��,�7�a�������^�!�#t/�<�;T!��O�iV?y�w�����(�"��Tp����*�Bcb�|j��	���^
�V�)` �L y��XY�������!LS-���-����k�~��K��j�o�������b8�.�{��3�8�F��W���%���K��������#���$6;�+�:��&|�/�*�o������]�@�����^��8�%f#/_����2������;�_�I����n(����k�2����� 7�� u|}�Hx������O>1lBnL��d���[|���~E&��mZ�u@�dV�6����[�����>4�S��G�'���x�sx1�*F�b[��U��BSQ/f�Q�<+��O>���������2\R�a{���$�{7�����n��b5Z�� V��Yo�LG[3���8��*�=��h����������� [{X�N�[b��r�9_�;A� ��c��4�Z.��;���#x�F���u�)7
@�O&��tA>���+����M�u��/���X������??�?�U�.% ��rS�+0�2n@%����/���������8 ��-|C'�:� �����7?���G��#�Z�CP�@�O���p~5�pA>~�kp�|�j ���������L2��s��j;n�Xx�|���=>���qO}O3u��?����Bs�P��Kn��At>�y��('���0����uj�bD�T�/!@��0B7��:��IS������'i�r�d�L��;r���|���������`v�0#�n*���V-��4�Icww?���j����I���\���j�����������6�����jOH�G{���nn�M����_���X
X�|���3Eh�����Yo���F���2Z-�����"d��X����g4���-Z������N�5$'����o�L��(f�c��\����I<�����[SF;[2��|�4����]��kY��)��3�'����$����s�N��+��!p:Fc��&�`��$���K��������H&�w+�j�E���~��V:��������+�m�)����jh=�����U�F�s�����z8�TN�$��I������5��5���p�
�w�;���3P��,���a�I��	1-"�}p�J%�� �����#��q��8�9Hc������������d6e����-�����������RK���teU
0����_�C������fb�leQ|$�����P�F��iL�C6[�fg3e��`�f+�6�q��Y��T��F��/`��P�L7CiT��d���4?�8C��=pB3������k����^��)�>��t&�&^\��_�f�3�����*oBv��5�Z��t�%d{�d�bW�����������������:�����9��l���|�?uF��(��d!��������{$����������������KZ��/�
|�|�|�|�|�|�|�|�|��'O�E:�}B�}B�����i���i��|�4�5$�~���/����T��f�~��������rN�sF��9���h��F22�8r����Ic����s������_:1���A�A�����1q�)�
0011-BRIN-multi-range-minmax-indexes.patch.gzapplication/gzipDownload
#49Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Tomas Vondra (#48)
Re: WIP: BRIN multi-range indexes

This patch fails to apply (or the opclass params one, maybe). Please
update.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#50Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alvaro Herrera (#49)
Re: WIP: BRIN multi-range indexes

On Wed, Sep 25, 2019 at 05:07:48PM -0300, Alvaro Herrera wrote:

This patch fails to apply (or the opclass params one, maybe). Please
update.

Yeah, the opclass params patches got broken by 773df883e adding enum
reloptions. The breakage is somewhat extensive so I'll leave it up to
Nikita to fix it in [1]https://commitfest.postgresql.org/24/2183/. Until that happens, apply the patches on
top of caba97a9d9 for review.

Thanks

[1]: https://commitfest.postgresql.org/24/2183/

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#51Michael Paquier
michael@paquier.xyz
In reply to: Tomas Vondra (#50)
Re: WIP: BRIN multi-range indexes

On Thu, Sep 26, 2019 at 09:01:48PM +0200, Tomas Vondra wrote:

Yeah, the opclass params patches got broken by 773df883e adding enum
reloptions. The breakage is somewhat extensive so I'll leave it up to
Nikita to fix it in [1]. Until that happens, apply the patches on
top of caba97a9d9 for review.

This has been close to two months now, so I have the patch as RwF.
Feel free to update if you think that's incorrect.
--
Michael

#52Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Michael Paquier (#51)
5 attachment(s)
Re: WIP: BRIN multi-range indexes

On Sun, Dec 01, 2019 at 10:55:02AM +0900, Michael Paquier wrote:

On Thu, Sep 26, 2019 at 09:01:48PM +0200, Tomas Vondra wrote:

Yeah, the opclass params patches got broken by 773df883e adding enum
reloptions. The breakage is somewhat extensive so I'll leave it up to
Nikita to fix it in [1]. Until that happens, apply the patches on
top of caba97a9d9 for review.

This has been close to two months now, so I have the patch as RwF.
Feel free to update if you think that's incorrect.

I see the opclass parameters patch got committed a couple days ago, so
I've rebased the patch series on top of it. The pach was marked RwF
since 2019-11, so I'll add it to the next CF.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20200402.patchtext/plain; charset=us-asciiDownload
From 54752ea9c0461053894f27252624426c8e598b14 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:05:39 +0200
Subject: [PATCH 2/5] Move IS NOT NULL checks to bringetbitmap

---
 src/backend/access/brin/brin.c           | 116 ++++++++++++++++++++---
 src/backend/access/brin/brin_inclusion.c |  62 +-----------
 src/backend/access/brin/brin_minmax.c    |  62 +-----------
 3 files changed, 109 insertions(+), 131 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index f1beb2eff9..25ae02a999 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -388,8 +388,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -414,10 +416,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,14 +444,12 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 							  keyattno - 1)->attcollation));
 
 		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -454,9 +457,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,6 +560,83 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys, and
+					 * if we're violating them. In that case we can terminate
+					 * early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					{
+						ScanKey	key = nullkeys[attno - 1][keyno];
+
+						Assert(key->sk_attno == bval->bv_attno);
+
+						/* interrupt the loop as soon as we find a mismatch */
+						if (!addrange)
+							break;
+
+						/* handle IS NULL/IS NOT NULL tests */
+						if (key->sk_flags & SK_ISNULL)
+						{
+							/* IS NULL scan key, but range has no NULLs */
+							if (key->sk_flags & SK_SEARCHNULL)
+							{
+								if (!bval->bv_allnulls && !bval->bv_hasnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * For IS NOT NULL, we can only skip ranges that are
+							 * known to have only nulls.
+							 */
+							if (key->sk_flags & SK_SEARCHNOTNULL)
+							{
+								if (bval->bv_allnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * Neither IS NULL nor IS NOT NULL was used; assume all
+							 * indexable operators are strict and thus return false
+							 * with NULL value in the scan key.
+							 */
+							addrange = false;
+						}
+					}
+
+					/*
+					 * If any of the IS [NOT] NULL keys failed, the page range as
+					 * a whole can't pass. So terminate the loop.
+					 */
+					if (!addrange)
+						break;
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed. If
+					 * there are no regular scan keys, we're done - the page range
+					 * matches. If there are regular keys, but the page range is
+					 * marked as 'all nulls' it can't possibly pass (we're assuming
+					 * the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 8968886ff5..22edc6b46f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -265,63 +265,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
 	bool		matches;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -333,9 +276,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = inclusion_consistent_key(bdesc, column, key, colloid);
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 1219a3a2ab..7a7bd21cec 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -153,63 +153,6 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	matches = true;
 
@@ -217,9 +160,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = minmax_consistent_key(bdesc, column, key, colloid);
 
-- 
2.21.1

0001-Pass-all-keys-to-BRIN-consistent-function-a-20200402.patchtext/plain; charset=us-asciiDownload
From 7c2adfa6d9195812edbeb46068b48de9a8b29f95 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 13 Sep 2019 18:34:39 +0200
Subject: [PATCH 1/5] Pass all keys to BRIN consistent function at once

---
 src/backend/access/brin/brin.c           | 126 ++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 164 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 116 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 293 insertions(+), 121 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 7db3ae5ee0..f1beb2eff9 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -388,6 +388,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -409,6 +412,53 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -469,7 +519,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -479,34 +529,19 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (!nkeys[attno - 1])
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
@@ -518,12 +553,43 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					 * the range as a whole, so break out of the loop as soon
 					 * as a false return value is obtained.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/* Check keys one by one */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..8968886ff5 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,103 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		matches;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = inclusion_consistent_key(bdesc, column, key, colloid);
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_BOOL(matches);
+}
+
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +376,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +437,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +458,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +477,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +512,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +522,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..1219a3a2ab 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,99 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
-	FmgrInfo   *finfo;
+	int			keyno;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = minmax_consistent_key(bdesc, column, key, colloid);
+
+		/* found non-matching key */
+		if (!matches)
+			break;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(matches);
+}
+
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +283,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 55ef716858..26ba8c6f98 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8039,7 +8039,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8055,7 +8055,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.21.1

0003-Move-processing-of-NULLs-from-BRIN-support--20200402.patchtext/plain; charset=us-asciiDownload
From 3fda6fd27c07a11f737dea4afe634afdbd03ecc6 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:56:00 +0200
Subject: [PATCH 3/5] Move processing of NULLs from BRIN support functions

---
 src/backend/access/brin/brin.c           | 260 ++++++++++++++---------
 src/backend/access/brin/brin_inclusion.c |  44 +---
 src/backend/access/brin/brin_minmax.c    |  41 +---
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 169 insertions(+), 179 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 25ae02a999..8c94252567 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -177,7 +180,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -241,31 +243,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -358,6 +336,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -561,69 +540,31 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
-					 * First check if there are any IS [NOT] NULL scan keys, and
-					 * if we're violating them. In that case we can terminate
-					 * early, without invoking the support function.
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
 					 *
 					 * As there may be more keys, we can only detemine mismatch
 					 * within this loop.
 					 */
-					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
 					{
-						ScanKey	key = nullkeys[attno - 1][keyno];
-
-						Assert(key->sk_attno == bval->bv_attno);
-
-						/* interrupt the loop as soon as we find a mismatch */
-						if (!addrange)
-							break;
-
-						/* handle IS NULL/IS NOT NULL tests */
-						if (key->sk_flags & SK_ISNULL)
-						{
-							/* IS NULL scan key, but range has no NULLs */
-							if (key->sk_flags & SK_SEARCHNULL)
-							{
-								if (!bval->bv_allnulls && !bval->bv_hasnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * For IS NOT NULL, we can only skip ranges that are
-							 * known to have only nulls.
-							 */
-							if (key->sk_flags & SK_SEARCHNOTNULL)
-							{
-								if (bval->bv_allnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * Neither IS NULL nor IS NOT NULL was used; assume all
-							 * indexable operators are strict and thus return false
-							 * with NULL value in the scan key.
-							 */
-							addrange = false;
-						}
-					}
-
-					/*
-					 * If any of the IS [NOT] NULL keys failed, the page range as
-					 * a whole can't pass. So terminate the loop.
-					 */
-					if (!addrange)
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
 						break;
+					}
 
 					/*
-					 * So either there are no IS [NOT] NULL keys, or all passed. If
-					 * there are no regular scan keys, we're done - the page range
-					 * matches. If there are regular keys, but the page range is
-					 * marked as 'all nulls' it can't possibly pass (we're assuming
-					 * the operators are strict).
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
 					 */
 
 					/* No regular scan keys - page range as a whole passes. */
@@ -771,7 +712,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -800,25 +740,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1597,6 +1520,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1650,3 +1606,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 22edc6b46f..59503b6f68 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -510,37 +500,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 7a7bd21cec..8882eec12c 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -245,34 +235,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.21.1

0004-BRIN-bloom-indexes-20200402.patchtext/plain; charset=us-asciiDownload
From 2928b78a101e9bd309a18888b508636bee981c3f Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:57:59 +0200
Subject: [PATCH 4/5] BRIN bloom indexes

---
 doc/src/sgml/brin.sgml                   | 215 +++++
 doc/src/sgml/ref/create_index.sgml       |  31 +
 src/backend/access/brin/Makefile         |   1 +
 src/backend/access/brin/brin_bloom.c     | 980 +++++++++++++++++++++++
 src/include/access/brin.h                |   2 +
 src/include/access/brin_internal.h       |   4 +
 src/include/catalog/pg_amop.dat          | 165 ++++
 src/include/catalog/pg_amproc.dat        | 430 ++++++++++
 src/include/catalog/pg_opclass.dat       |  69 ++
 src/include/catalog/pg_opfamily.dat      |  36 +
 src/include/catalog/pg_proc.dat          |  20 +
 src/test/regress/expected/brin_bloom.out | 456 +++++++++++
 src/test/regress/expected/opr_sanity.out |   3 +-
 src/test/regress/parallel_schedule       |   5 +
 src/test/regress/serial_schedule         |   1 +
 src/test/regress/sql/brin_bloom.sql      | 404 ++++++++++
 16 files changed, 2821 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 176f1cfbd0..ba73966112 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -129,6 +129,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry><literal>int8_bloom_ops</literal></entry>
+     <entry><type>bigint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int8_minmax_ops</literal></entry>
      <entry><type>bigint</type></entry>
@@ -180,6 +187,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>|&amp;&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>bytea_bloom_ops</literal></entry>
+     <entry><type>bytea</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>bytea_minmax_ops</literal></entry>
      <entry><type>bytea</type></entry>
@@ -191,6 +205,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>bpchar_bloom_ops</literal></entry>
+     <entry><type>character</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>bpchar_minmax_ops</literal></entry>
      <entry><type>character</type></entry>
@@ -202,6 +223,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>char_bloom_ops</literal></entry>
+     <entry><type>"char"</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>char_minmax_ops</literal></entry>
      <entry><type>"char"</type></entry>
@@ -213,6 +241,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>date_bloom_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>date_minmax_ops</literal></entry>
      <entry><type>date</type></entry>
@@ -224,6 +259,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float8_bloom_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float8_minmax_ops</literal></entry>
      <entry><type>double precision</type></entry>
@@ -235,6 +277,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>inet_bloom_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>inet_minmax_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -258,6 +307,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&lt;&lt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>int4_bloom_ops</literal></entry>
+     <entry><type>integer</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int4_minmax_ops</literal></entry>
      <entry><type>integer</type></entry>
@@ -269,6 +325,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>interval_bloom_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>interval_minmax_ops</literal></entry>
      <entry><type>interval</type></entry>
@@ -280,6 +343,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr_bloom_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr_minmax_ops</literal></entry>
      <entry><type>macaddr</type></entry>
@@ -291,6 +361,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr8_minmax_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
@@ -302,6 +379,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>name_bloom_ops</literal></entry>
+     <entry><type>name</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>name_minmax_ops</literal></entry>
      <entry><type>name</type></entry>
@@ -313,6 +397,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>numeric_bloom_ops</literal></entry>
+     <entry><type>numeric</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>numeric_minmax_ops</literal></entry>
      <entry><type>numeric</type></entry>
@@ -324,6 +415,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><type>pg_lsn</type></entry>
@@ -335,6 +433,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>oid_bloom_ops</literal></entry>
+     <entry><type>oid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>oid_minmax_ops</literal></entry>
      <entry><type>oid</type></entry>
@@ -366,6 +471,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;=</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float4_bloom_ops</literal></entry>
+     <entry><type>real</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float4_minmax_ops</literal></entry>
      <entry><type>real</type></entry>
@@ -377,6 +489,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>int2_bloom_ops</literal></entry>
+     <entry><type>smallint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int2_minmax_ops</literal></entry>
      <entry><type>smallint</type></entry>
@@ -388,6 +507,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>text_bloom_ops</literal></entry>
+     <entry><type>text</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>text_minmax_ops</literal></entry>
      <entry><type>text</type></entry>
@@ -410,6 +536,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamp_bloom_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamp_minmax_ops</literal></entry>
      <entry><type>timestamp without time zone</type></entry>
@@ -421,6 +554,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamptz_minmax_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
@@ -432,6 +572,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>time_bloom_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>time_minmax_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
@@ -443,6 +590,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timetz_bloom_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timetz_minmax_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
@@ -454,6 +608,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>uuid_bloom_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>uuid_minmax_ops</literal></entry>
      <entry><type>uuid</type></entry>
@@ -803,6 +964,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index aaf087e2e3..2e3e837faa 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -556,6 +556,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..b119e4e264
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,980 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i,
+			nvalues;
+	Size	len;
+	uint32 *values;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* TODO optimization: sort only the unsorted part, then merge */
+	pg_qsort(values, filter->nvalues, sizeof(uint32), cmp_uint32);
+
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (values[i] != values[nvalues - 1])
+			values[nvalues++] = values[i];
+	}
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 11aaa519c8..3de5455221 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1682,6 +1682,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1699,6 +1704,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1716,6 +1726,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1862,6 +1877,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1879,6 +1932,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1896,6 +1954,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1979,6 +2042,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -1996,6 +2073,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2013,6 +2095,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2030,6 +2117,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2067,6 +2159,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2084,6 +2181,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2230,6 +2332,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2247,6 +2387,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2264,6 +2409,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2315,6 +2465,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2332,6 +2487,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2393,6 +2553,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index cef63b2a71..6d5b6fb455 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -762,6 +762,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -775,6 +793,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -788,6 +824,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -889,6 +943,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -902,6 +1008,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -914,6 +1038,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -976,6 +1117,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -990,6 +1170,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1004,6 +1204,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1017,6 +1237,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1051,6 +1289,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1064,6 +1322,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1171,6 +1447,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1185,6 +1517,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1199,6 +1551,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1239,6 +1611,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1252,6 +1644,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1287,6 +1697,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '0',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index ab2f50c9eb..092511df81 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -253,67 +253,127 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -322,18 +382,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 26227df216..ba428ba379 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -176,50 +176,86 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '5024',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '5027',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '5045',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '5042',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '5038',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '5022',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '5048',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '5023',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '5049',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '5050',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '5033',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '5034',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '5035',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '5036',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '5037',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '5041',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '5046',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '5047',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 26ba8c6f98..fa908278d2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8062,6 +8062,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '5039', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '5040', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '5043', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '5044', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '6016', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 2efd7d7ec7..cd65ae5c22 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -1997,6 +1997,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2060,7 +2061,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index d2b17dd3ea..739303dbbd 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom 
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index acba391332..3e3e537951 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -104,6 +104,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.21.1

0005-BRIN-multi-range-minmax-indexes-20200402.patchtext/plain; charset=us-asciiDownload
From 4b53a180ac4dc9eac7c221ee40b484f2b1c53273 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:59:25 +0200
Subject: [PATCH 5/5] BRIN multi-range minmax indexes

---
 doc/src/sgml/brin.sgml                      |  212 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2178 +++++++++++++++++++
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/transam.h                |    8 +-
 src/include/catalog/pg_amop.dat             |  545 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   30 +-
 src/include/catalog/pg_proc.dat             |   65 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 ++++
 16 files changed, 4492 insertions(+), 14 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index ba73966112..15335adbd1 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -259,6 +259,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>date_minmax_multi_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float8_bloom_ops</literal></entry>
      <entry><type>double precision</type></entry>
@@ -277,6 +288,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>inet_bloom_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -295,6 +317,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>network_inclusion_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -343,6 +376,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr_bloom_ops</literal></entry>
      <entry><type>macaddr</type></entry>
@@ -361,6 +405,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr8_bloom_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
@@ -379,6 +434,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>name_bloom_ops</literal></entry>
      <entry><type>name</type></entry>
@@ -433,6 +499,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>oid_bloom_ops</literal></entry>
      <entry><type>oid</type></entry>
@@ -554,6 +631,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamptz_bloom_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
@@ -572,6 +660,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>time_bloom_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
@@ -590,6 +689,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>time_minmax_multi_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timetz_bloom_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
@@ -608,6 +718,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>uuid_bloom_ops</literal></entry>
      <entry><type>uuid</type></entry>
@@ -626,6 +747,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
    </tbody>
   </tgroup>
  </table>
@@ -720,13 +852,13 @@ typedef struct BrinOpcInfo
    </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions using
+  them are shipped for in-core data types as appropriate.  Additional operator
+  classes can be defined by the user for other data types using equivalent
+  definitions, without having to write any source code; appropriate catalog
+  entries being declared is enough.  Note that assumptions about the semantics
+  of operator strategies are embedded in the support functions' source code.
  </para>
 
  <para>
@@ -795,6 +927,72 @@ typedef struct BrinOpcInfo
   </tgroup>
  </table>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
   To write an operator class for a complex data type which has values
   included within another type, it's possible to use the inclusion support
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 2e3e837faa..6184404015 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -587,6 +587,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 64.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..9b9eedbb63
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2178 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+static Ranges *range_deserialize(SerializedRanges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (attr->attlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (attr->attlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(attr->attlen > 0);
+		len += nvalues * attr->attlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized,
+				AttrNumber attno, Form_pg_attribute attr)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTLessStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count_values(cranges, ncranges) <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		FmgrInfo   *cmpFn;
+		Datum	   *values;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												   BTLessStrategyNumber);
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		column->bv_allnulls = false;
+		modified = true;
+	}
+	else
+	{
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized, attno, attr);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+	if (modified)
+	{
+		SerializedRanges *s = range_serialize(ranges, attno, attr);
+		column->bv_values[0] = PointerGetDatum(s);
+
+		/*
+		 * XXX pfree must happen after range_serialize, because the Ranges value
+		 * may reference the original serialized value.
+		 */
+		if (serialized)
+			pfree(serialized);
+	}
+
+	pfree(ranges);
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+	Form_pg_attribute attr;
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized, attno, attr);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a, attno, attr);
+	ranges_b = range_deserialize(serialized_b, attno, attr);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attno, attr));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 6a947b958b..7f2710a28d 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -116,14 +116,14 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  *		when the .dat files in src/include/catalog/ do not specify an OID
  *		for a catalog entry that requires one.
  *
- *		OIDS 12000-16383 are reserved for assignment during initdb
- *		using the OID generator.  (We start the generator at 12000.)
+ *		OIDS 13000-16383 are reserved for assignment during initdb
+ *		using the OID generator.  (We start the generator at 13000.)
  *
  *		OIDs beginning at 16384 are assigned from the OID generator
  *		during normal multiuser operation.  (We force the generator up to
  *		16384 as soon as we are in normal operation.)
  *
- * The choices of 8000, 10000 and 12000 are completely arbitrary, and can be
+ * The choices of 8000, 10000 and 13000 are completely arbitrary, and can be
  * moved if we run low on OIDs in any category.  Changing the macros below,
  * and updating relevant documentation (see bki.sgml and RELEASE_CHANGES),
  * should be sufficient to do this.  Moving the 16384 boundary between
@@ -137,7 +137,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 3de5455221..6845887730 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1877,6 +1877,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1954,6 +2100,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -1976,6 +2139,23 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2042,6 +2222,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2073,6 +2319,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2095,6 +2358,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2117,6 +2397,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2181,6 +2478,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2332,6 +2646,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2387,6 +2847,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2409,6 +2886,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2465,6 +2959,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2487,6 +2998,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2553,6 +3081,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 6d5b6fb455..1970fd08d6 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -943,6 +943,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1038,6 +1184,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1067,6 +1230,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1117,6 +1297,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1170,6 +1424,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1204,6 +1478,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1237,6 +1531,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1322,6 +1636,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1447,6 +1780,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1517,6 +2014,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1551,6 +2068,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1611,6 +2148,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1642,7 +2199,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1697,6 +2275,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '0',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 092511df81..0572d529e7 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -271,18 +271,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -294,38 +303,59 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -341,36 +371,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -382,6 +430,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -391,6 +442,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -400,6 +454,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index ba428ba379..3f03ec6207 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -176,10 +176,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '5054',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '5024',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '5087',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '5027',
@@ -188,10 +192,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '5059',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '5042',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '5088',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '5038',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -209,23 +217,35 @@
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
 { oid => '5049',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
+{ oid => '5051',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '5089',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
-{ oid => '5050',
+{ oid => '5091',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
+{ oid => '5052',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '5064',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '5033',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '5063',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '5034',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '5066',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '5035',
@@ -236,10 +256,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '5090',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '5037',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '5061',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '5041',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -248,12 +272,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '5050',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '5046',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '5065',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '5047',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa908278d2..4c6914ad81 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8045,6 +8045,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '5068', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '5069', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '5070', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '5071', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '6015', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'bool', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '5072', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '5073', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '5074', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '5075', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '5076', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '5077', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '5078', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '5079', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '5080', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '5081', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '5082', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '5083', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '5084', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '5085', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '5086', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 739303dbbd..ac03cf1408 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom 
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 3e3e537951..fb3bb1204e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -105,6 +105,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.21.1

#53Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: Tomas Vondra (#52)
Re: WIP: BRIN multi-range indexes

Hi!

On Thu, Apr 2, 2020 at 5:29 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Sun, Dec 01, 2019 at 10:55:02AM +0900, Michael Paquier wrote:

On Thu, Sep 26, 2019 at 09:01:48PM +0200, Tomas Vondra wrote:

Yeah, the opclass params patches got broken by 773df883e adding enum
reloptions. The breakage is somewhat extensive so I'll leave it up to
Nikita to fix it in [1]. Until that happens, apply the patches on
top of caba97a9d9 for review.

This has been close to two months now, so I have the patch as RwF.
Feel free to update if you think that's incorrect.

I see the opclass parameters patch got committed a couple days ago, so
I've rebased the patch series on top of it. The pach was marked RwF
since 2019-11, so I'll add it to the next CF.

I think this patchset was marked RwF mainly because slow progress on
opclass parameters. Now we got opclass parameters committed, and I
think this patchset is in a pretty good shape. Moreover, opclass
parameters patch comes with very small examples. This patchset would
be great showcase for opclass parameters.

I'd like to give this patchset a chance for v13. I'm going to make
another pass trough this patchset. If I wouldn't find serious issues,
I'm going to commit it. Any objections?

------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#54Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alexander Korotkov (#53)
Re: WIP: BRIN multi-range indexes

Alexander Korotkov <a.korotkov@postgrespro.ru> writes:

I'd like to give this patchset a chance for v13. I'm going to make
another pass trough this patchset. If I wouldn't find serious issues,
I'm going to commit it. Any objections?

I think it is way too late to be reviving major features that nobody
has been looking at for months, that indeed were never even listed
in the final CF. At this point in the cycle I think we should just be
trying to get small stuff over the line, not shove in major patches
and figure they can be stabilized later.

In this particular case, the last serious work on the patchset seems
to have been Tomas' revision of 2019-09-14, and he specifically stated
then that the APIs still needed work. That doesn't sound like
"it's about ready to commit" to me.

regards, tom lane

#55Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alexander Korotkov (#53)
Re: WIP: BRIN multi-range indexes

On Sun, Apr 05, 2020 at 06:29:15PM +0300, Alexander Korotkov wrote:

Hi!

On Thu, Apr 2, 2020 at 5:29 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Sun, Dec 01, 2019 at 10:55:02AM +0900, Michael Paquier wrote:

On Thu, Sep 26, 2019 at 09:01:48PM +0200, Tomas Vondra wrote:

Yeah, the opclass params patches got broken by 773df883e adding enum
reloptions. The breakage is somewhat extensive so I'll leave it up to
Nikita to fix it in [1]. Until that happens, apply the patches on
top of caba97a9d9 for review.

This has been close to two months now, so I have the patch as RwF.
Feel free to update if you think that's incorrect.

I see the opclass parameters patch got committed a couple days ago, so
I've rebased the patch series on top of it. The pach was marked RwF
since 2019-11, so I'll add it to the next CF.

I think this patchset was marked RwF mainly because slow progress on
opclass parameters. Now we got opclass parameters committed, and I
think this patchset is in a pretty good shape. Moreover, opclass
parameters patch comes with very small examples. This patchset would
be great showcase for opclass parameters.

I'd like to give this patchset a chance for v13. I'm going to make
another pass trough this patchset. If I wouldn't find serious issues,
I'm going to commit it. Any objections?

I'm an author of the patchset and I'd love to see it committed, but I
think that might be a bit too rushed and unfair (considering it was not
included in the current CF at all).

I think the code is correct and I'm not aware of any bugs, but I'm not
sure there was sufficient discussion about things like costing, choosing
parameter values (e.g. number of values in the multi-minmax or bloom
filter parameters).

That being said, I think the first couple of patches (that modify how
BRIN deals with multi-key scans and IS NULL clauses) are simple enough
and non-controversial, so maybe we could get 0001-0003 committed, and
leave the bloom/multi-minmax opclasses for v14.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#56Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: Tom Lane (#54)
Re: WIP: BRIN multi-range indexes

On Sun, Apr 5, 2020 at 6:51 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexander Korotkov <a.korotkov@postgrespro.ru> writes:

I'd like to give this patchset a chance for v13. I'm going to make
another pass trough this patchset. If I wouldn't find serious issues,
I'm going to commit it. Any objections?

I think it is way too late to be reviving major features that nobody
has been looking at for months, that indeed were never even listed
in the final CF. At this point in the cycle I think we should just be
trying to get small stuff over the line, not shove in major patches
and figure they can be stabilized later.

In this particular case, the last serious work on the patchset seems
to have been Tomas' revision of 2019-09-14, and he specifically stated
then that the APIs still needed work. That doesn't sound like
"it's about ready to commit" to me.

OK, got it. Thank you for the feedback.

------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#57Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: Tomas Vondra (#55)
Re: WIP: BRIN multi-range indexes

On Sun, Apr 5, 2020 at 6:53 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Sun, Apr 05, 2020 at 06:29:15PM +0300, Alexander Korotkov wrote:

On Thu, Apr 2, 2020 at 5:29 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Sun, Dec 01, 2019 at 10:55:02AM +0900, Michael Paquier wrote:

On Thu, Sep 26, 2019 at 09:01:48PM +0200, Tomas Vondra wrote:

Yeah, the opclass params patches got broken by 773df883e adding enum
reloptions. The breakage is somewhat extensive so I'll leave it up to
Nikita to fix it in [1]. Until that happens, apply the patches on
top of caba97a9d9 for review.

This has been close to two months now, so I have the patch as RwF.
Feel free to update if you think that's incorrect.

I see the opclass parameters patch got committed a couple days ago, so
I've rebased the patch series on top of it. The pach was marked RwF
since 2019-11, so I'll add it to the next CF.

I think this patchset was marked RwF mainly because slow progress on
opclass parameters. Now we got opclass parameters committed, and I
think this patchset is in a pretty good shape. Moreover, opclass
parameters patch comes with very small examples. This patchset would
be great showcase for opclass parameters.

I'd like to give this patchset a chance for v13. I'm going to make
another pass trough this patchset. If I wouldn't find serious issues,
I'm going to commit it. Any objections?

I'm an author of the patchset and I'd love to see it committed, but I
think that might be a bit too rushed and unfair (considering it was not
included in the current CF at all).

I think the code is correct and I'm not aware of any bugs, but I'm not
sure there was sufficient discussion about things like costing, choosing
parameter values (e.g. number of values in the multi-minmax or bloom
filter parameters).

Ok!

That being said, I think the first couple of patches (that modify how
BRIN deals with multi-key scans and IS NULL clauses) are simple enough
and non-controversial, so maybe we could get 0001-0003 committed, and
leave the bloom/multi-minmax opclasses for v14.

Regarding 0001-0003 I've couple of notes:
1) They should revise BRIN extensibility documentation section.
2) I think 0002 and 0003 should be merged. NULL ScanKeys should be
still passed to consistent function when oi_regular_nulls == false.

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing to commit
them, you can count round of review on me.

------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#58Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alexander Korotkov (#57)
Re: WIP: BRIN multi-range indexes

On Sun, Apr 05, 2020 at 07:33:40PM +0300, Alexander Korotkov wrote:

On Sun, Apr 5, 2020 at 6:53 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Sun, Apr 05, 2020 at 06:29:15PM +0300, Alexander Korotkov wrote:

On Thu, Apr 2, 2020 at 5:29 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Sun, Dec 01, 2019 at 10:55:02AM +0900, Michael Paquier wrote:

On Thu, Sep 26, 2019 at 09:01:48PM +0200, Tomas Vondra wrote:

Yeah, the opclass params patches got broken by 773df883e adding enum
reloptions. The breakage is somewhat extensive so I'll leave it up to
Nikita to fix it in [1]. Until that happens, apply the patches on
top of caba97a9d9 for review.

This has been close to two months now, so I have the patch as RwF.
Feel free to update if you think that's incorrect.

I see the opclass parameters patch got committed a couple days ago, so
I've rebased the patch series on top of it. The pach was marked RwF
since 2019-11, so I'll add it to the next CF.

I think this patchset was marked RwF mainly because slow progress on
opclass parameters. Now we got opclass parameters committed, and I
think this patchset is in a pretty good shape. Moreover, opclass
parameters patch comes with very small examples. This patchset would
be great showcase for opclass parameters.

I'd like to give this patchset a chance for v13. I'm going to make
another pass trough this patchset. If I wouldn't find serious issues,
I'm going to commit it. Any objections?

I'm an author of the patchset and I'd love to see it committed, but I
think that might be a bit too rushed and unfair (considering it was not
included in the current CF at all).

I think the code is correct and I'm not aware of any bugs, but I'm not
sure there was sufficient discussion about things like costing, choosing
parameter values (e.g. number of values in the multi-minmax or bloom
filter parameters).

Ok!

That being said, I think the first couple of patches (that modify how
BRIN deals with multi-key scans and IS NULL clauses) are simple enough
and non-controversial, so maybe we could get 0001-0003 committed, and
leave the bloom/multi-minmax opclasses for v14.

Regarding 0001-0003 I've couple of notes:
1) They should revise BRIN extensibility documentation section.
2) I think 0002 and 0003 should be merged. NULL ScanKeys should be
still passed to consistent function when oi_regular_nulls == false.

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing to commit
them, you can count round of review on me.

I have no intention to get 0001-0003 committed. I think those changes
are beneficial on their own, but the primary reason was to support the
new opclasses (which require those changes). And those parts are not
going to make it into v13 ...

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#59Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: Tomas Vondra (#58)
Re: WIP: BRIN multi-range indexes

On Sun, Apr 5, 2020 at 8:00 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Sun, Apr 05, 2020 at 07:33:40PM +0300, Alexander Korotkov wrote:

On Sun, Apr 5, 2020 at 6:53 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Sun, Apr 05, 2020 at 06:29:15PM +0300, Alexander Korotkov wrote:

On Thu, Apr 2, 2020 at 5:29 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Sun, Dec 01, 2019 at 10:55:02AM +0900, Michael Paquier wrote:

On Thu, Sep 26, 2019 at 09:01:48PM +0200, Tomas Vondra wrote:

Yeah, the opclass params patches got broken by 773df883e adding enum
reloptions. The breakage is somewhat extensive so I'll leave it up to
Nikita to fix it in [1]. Until that happens, apply the patches on
top of caba97a9d9 for review.

This has been close to two months now, so I have the patch as RwF.
Feel free to update if you think that's incorrect.

I see the opclass parameters patch got committed a couple days ago, so
I've rebased the patch series on top of it. The pach was marked RwF
since 2019-11, so I'll add it to the next CF.

I think this patchset was marked RwF mainly because slow progress on
opclass parameters. Now we got opclass parameters committed, and I
think this patchset is in a pretty good shape. Moreover, opclass
parameters patch comes with very small examples. This patchset would
be great showcase for opclass parameters.

I'd like to give this patchset a chance for v13. I'm going to make
another pass trough this patchset. If I wouldn't find serious issues,
I'm going to commit it. Any objections?

I'm an author of the patchset and I'd love to see it committed, but I
think that might be a bit too rushed and unfair (considering it was not
included in the current CF at all).

I think the code is correct and I'm not aware of any bugs, but I'm not
sure there was sufficient discussion about things like costing, choosing
parameter values (e.g. number of values in the multi-minmax or bloom
filter parameters).

Ok!

That being said, I think the first couple of patches (that modify how
BRIN deals with multi-key scans and IS NULL clauses) are simple enough
and non-controversial, so maybe we could get 0001-0003 committed, and
leave the bloom/multi-minmax opclasses for v14.

Regarding 0001-0003 I've couple of notes:
1) They should revise BRIN extensibility documentation section.
2) I think 0002 and 0003 should be merged. NULL ScanKeys should be
still passed to consistent function when oi_regular_nulls == false.

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing to commit
them, you can count round of review on me.

I have no intention to get 0001-0003 committed. I think those changes
are beneficial on their own, but the primary reason was to support the
new opclasses (which require those changes). And those parts are not
going to make it into v13 ...

OK, no problem.
Let's do this for v14.

------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#60Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alexander Korotkov (#59)
4 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

here is an updated patch series, fixing duplicate OIDs etc.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-a-20200513.patchtext/plain; charset=us-asciiDownload
From 11accee6a5b20af4e09382f90912dd72e79d832a Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 13 Sep 2019 18:34:39 +0200
Subject: [PATCH 1/5] Pass all keys to BRIN consistent function at once

---
 src/backend/access/brin/brin.c           | 126 ++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 164 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 116 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 293 insertions(+), 121 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 7db3ae5ee0..f1beb2eff9 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -388,6 +388,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -409,6 +412,53 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -469,7 +519,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -479,34 +529,19 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (!nkeys[attno - 1])
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
@@ -518,12 +553,43 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					 * the range as a whole, so break out of the loop as soon
 					 * as a false return value is obtained.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/* Check keys one by one */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..8968886ff5 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,103 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		matches;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = inclusion_consistent_key(bdesc, column, key, colloid);
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_BOOL(matches);
+}
+
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +376,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +437,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +458,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +477,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +512,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +522,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..1219a3a2ab 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,99 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
-	FmgrInfo   *finfo;
+	int			keyno;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = minmax_consistent_key(bdesc, column, key, colloid);
+
+		/* found non-matching key */
+		if (!matches)
+			break;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(matches);
+}
+
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +283,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9edae40ed8..e5798da368 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8073,7 +8073,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8089,7 +8089,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.21.3

0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20200513.patchtext/plain; charset=us-asciiDownload
From 6a289c4ed126c3fdf31f0cc98e54d47cb7d80e2c Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:05:39 +0200
Subject: [PATCH 2/5] Move IS NOT NULL checks to bringetbitmap

---
 src/backend/access/brin/brin.c           | 116 ++++++++++++++++++++---
 src/backend/access/brin/brin_inclusion.c |  62 +-----------
 src/backend/access/brin/brin_minmax.c    |  62 +-----------
 3 files changed, 109 insertions(+), 131 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index f1beb2eff9..25ae02a999 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -388,8 +388,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -414,10 +416,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,14 +444,12 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 							  keyattno - 1)->attcollation));
 
 		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -454,9 +457,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,6 +560,83 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys, and
+					 * if we're violating them. In that case we can terminate
+					 * early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					{
+						ScanKey	key = nullkeys[attno - 1][keyno];
+
+						Assert(key->sk_attno == bval->bv_attno);
+
+						/* interrupt the loop as soon as we find a mismatch */
+						if (!addrange)
+							break;
+
+						/* handle IS NULL/IS NOT NULL tests */
+						if (key->sk_flags & SK_ISNULL)
+						{
+							/* IS NULL scan key, but range has no NULLs */
+							if (key->sk_flags & SK_SEARCHNULL)
+							{
+								if (!bval->bv_allnulls && !bval->bv_hasnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * For IS NOT NULL, we can only skip ranges that are
+							 * known to have only nulls.
+							 */
+							if (key->sk_flags & SK_SEARCHNOTNULL)
+							{
+								if (bval->bv_allnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * Neither IS NULL nor IS NOT NULL was used; assume all
+							 * indexable operators are strict and thus return false
+							 * with NULL value in the scan key.
+							 */
+							addrange = false;
+						}
+					}
+
+					/*
+					 * If any of the IS [NOT] NULL keys failed, the page range as
+					 * a whole can't pass. So terminate the loop.
+					 */
+					if (!addrange)
+						break;
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed. If
+					 * there are no regular scan keys, we're done - the page range
+					 * matches. If there are regular keys, but the page range is
+					 * marked as 'all nulls' it can't possibly pass (we're assuming
+					 * the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 8968886ff5..22edc6b46f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -265,63 +265,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
 	bool		matches;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -333,9 +276,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = inclusion_consistent_key(bdesc, column, key, colloid);
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 1219a3a2ab..7a7bd21cec 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -153,63 +153,6 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	matches = true;
 
@@ -217,9 +160,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = minmax_consistent_key(bdesc, column, key, colloid);
 
-- 
2.21.3

0003-Move-processing-of-NULLs-from-BRIN-support--20200513.patchtext/plain; charset=us-asciiDownload
From 61a3d76ebb6a3e4c36bbdea692de8ff766b66a93 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:56:00 +0200
Subject: [PATCH 3/5] Move processing of NULLs from BRIN support functions

---
 src/backend/access/brin/brin.c           | 260 ++++++++++++++---------
 src/backend/access/brin/brin_inclusion.c |  44 +---
 src/backend/access/brin/brin_minmax.c    |  41 +---
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 169 insertions(+), 179 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 25ae02a999..8c94252567 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -177,7 +180,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -241,31 +243,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -358,6 +336,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -561,69 +540,31 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
-					 * First check if there are any IS [NOT] NULL scan keys, and
-					 * if we're violating them. In that case we can terminate
-					 * early, without invoking the support function.
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
 					 *
 					 * As there may be more keys, we can only detemine mismatch
 					 * within this loop.
 					 */
-					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
 					{
-						ScanKey	key = nullkeys[attno - 1][keyno];
-
-						Assert(key->sk_attno == bval->bv_attno);
-
-						/* interrupt the loop as soon as we find a mismatch */
-						if (!addrange)
-							break;
-
-						/* handle IS NULL/IS NOT NULL tests */
-						if (key->sk_flags & SK_ISNULL)
-						{
-							/* IS NULL scan key, but range has no NULLs */
-							if (key->sk_flags & SK_SEARCHNULL)
-							{
-								if (!bval->bv_allnulls && !bval->bv_hasnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * For IS NOT NULL, we can only skip ranges that are
-							 * known to have only nulls.
-							 */
-							if (key->sk_flags & SK_SEARCHNOTNULL)
-							{
-								if (bval->bv_allnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * Neither IS NULL nor IS NOT NULL was used; assume all
-							 * indexable operators are strict and thus return false
-							 * with NULL value in the scan key.
-							 */
-							addrange = false;
-						}
-					}
-
-					/*
-					 * If any of the IS [NOT] NULL keys failed, the page range as
-					 * a whole can't pass. So terminate the loop.
-					 */
-					if (!addrange)
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
 						break;
+					}
 
 					/*
-					 * So either there are no IS [NOT] NULL keys, or all passed. If
-					 * there are no regular scan keys, we're done - the page range
-					 * matches. If there are regular keys, but the page range is
-					 * marked as 'all nulls' it can't possibly pass (we're assuming
-					 * the operators are strict).
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
 					 */
 
 					/* No regular scan keys - page range as a whole passes. */
@@ -771,7 +712,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -800,25 +740,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1597,6 +1520,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1650,3 +1606,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 22edc6b46f..59503b6f68 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -510,37 +500,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 7a7bd21cec..8882eec12c 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -245,34 +235,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.21.3

0004-BRIN-bloom-indexes-20200513.patchtext/plain; charset=us-asciiDownload
From 7962a4a3e6b77596fde4df6e4ff97bda19408c52 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:57:59 +0200
Subject: [PATCH 4/5] BRIN bloom indexes

---
 doc/src/sgml/brin.sgml                   | 215 +++++
 doc/src/sgml/ref/create_index.sgml       |  31 +
 src/backend/access/brin/Makefile         |   1 +
 src/backend/access/brin/brin_bloom.c     | 980 +++++++++++++++++++++++
 src/include/access/brin.h                |   2 +
 src/include/access/brin_internal.h       |   4 +
 src/include/catalog/pg_amop.dat          | 165 ++++
 src/include/catalog/pg_amproc.dat        | 430 ++++++++++
 src/include/catalog/pg_opclass.dat       |  69 ++
 src/include/catalog/pg_opfamily.dat      |  36 +
 src/include/catalog/pg_proc.dat          |  20 +
 src/test/regress/expected/brin_bloom.out | 456 +++++++++++
 src/test/regress/expected/opr_sanity.out |   3 +-
 src/test/regress/expected/psql.out       |   3 +-
 src/test/regress/parallel_schedule       |   5 +
 src/test/regress/serial_schedule         |   1 +
 src/test/regress/sql/brin_bloom.sql      | 404 ++++++++++
 17 files changed, 2823 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 46a7d07bf8..900cd47a4c 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -132,6 +132,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry><literal>int8_bloom_ops</literal></entry>
+     <entry><type>bigint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int8_minmax_ops</literal></entry>
      <entry><type>bigint</type></entry>
@@ -183,6 +190,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>|&amp;&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>bytea_bloom_ops</literal></entry>
+     <entry><type>bytea</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>bytea_minmax_ops</literal></entry>
      <entry><type>bytea</type></entry>
@@ -194,6 +208,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>bpchar_bloom_ops</literal></entry>
+     <entry><type>character</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>bpchar_minmax_ops</literal></entry>
      <entry><type>character</type></entry>
@@ -205,6 +226,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>char_bloom_ops</literal></entry>
+     <entry><type>"char"</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>char_minmax_ops</literal></entry>
      <entry><type>"char"</type></entry>
@@ -216,6 +244,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>date_bloom_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>date_minmax_ops</literal></entry>
      <entry><type>date</type></entry>
@@ -227,6 +262,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float8_bloom_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float8_minmax_ops</literal></entry>
      <entry><type>double precision</type></entry>
@@ -238,6 +280,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>inet_bloom_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>inet_minmax_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -261,6 +310,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&lt;&lt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>int4_bloom_ops</literal></entry>
+     <entry><type>integer</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int4_minmax_ops</literal></entry>
      <entry><type>integer</type></entry>
@@ -272,6 +328,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>interval_bloom_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>interval_minmax_ops</literal></entry>
      <entry><type>interval</type></entry>
@@ -283,6 +346,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr_bloom_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr_minmax_ops</literal></entry>
      <entry><type>macaddr</type></entry>
@@ -294,6 +364,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr8_minmax_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
@@ -305,6 +382,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>name_bloom_ops</literal></entry>
+     <entry><type>name</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>name_minmax_ops</literal></entry>
      <entry><type>name</type></entry>
@@ -316,6 +400,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>numeric_bloom_ops</literal></entry>
+     <entry><type>numeric</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>numeric_minmax_ops</literal></entry>
      <entry><type>numeric</type></entry>
@@ -327,6 +418,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><type>pg_lsn</type></entry>
@@ -338,6 +436,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>oid_bloom_ops</literal></entry>
+     <entry><type>oid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>oid_minmax_ops</literal></entry>
      <entry><type>oid</type></entry>
@@ -369,6 +474,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;=</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float4_bloom_ops</literal></entry>
+     <entry><type>real</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float4_minmax_ops</literal></entry>
      <entry><type>real</type></entry>
@@ -380,6 +492,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>int2_bloom_ops</literal></entry>
+     <entry><type>smallint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int2_minmax_ops</literal></entry>
      <entry><type>smallint</type></entry>
@@ -391,6 +510,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>text_bloom_ops</literal></entry>
+     <entry><type>text</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>text_minmax_ops</literal></entry>
      <entry><type>text</type></entry>
@@ -413,6 +539,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamp_bloom_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamp_minmax_ops</literal></entry>
      <entry><type>timestamp without time zone</type></entry>
@@ -424,6 +557,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamptz_minmax_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
@@ -435,6 +575,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>time_bloom_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>time_minmax_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
@@ -446,6 +593,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timetz_bloom_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timetz_minmax_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
@@ -457,6 +611,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>uuid_bloom_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>uuid_minmax_ops</literal></entry>
      <entry><type>uuid</type></entry>
@@ -811,6 +972,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index ff87b2d28f..f6380ef8e2 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -555,6 +555,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..b119e4e264
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,980 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i,
+			nvalues;
+	Size	len;
+	uint32 *values;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* TODO optimization: sort only the unsorted part, then merge */
+	pg_qsort(values, filter->nvalues, sizeof(uint32), cmp_uint32);
+
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (values[i] != values[nvalues - 1])
+			values[nvalues++] = values[i];
+	}
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..af6891e348 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2002,6 +2065,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2096,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2118,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2140,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2182,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2204,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2355,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2410,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2432,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2488,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2510,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2576,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 37b580883f..c24a80545a 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -770,6 +770,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -783,6 +801,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -796,6 +832,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -897,6 +951,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -910,6 +1016,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -922,6 +1046,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -984,6 +1125,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -998,6 +1178,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1012,6 +1212,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1025,6 +1245,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1059,6 +1297,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1072,6 +1330,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1179,6 +1455,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1193,6 +1525,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1207,6 +1559,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1247,6 +1619,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1260,6 +1652,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1295,6 +1705,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..16d6111ab6 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,127 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +386,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..7f733aeab1 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,86 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e5798da368..132a274bbc 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8096,6 +8096,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 27056d70d3..729d5c04c1 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2009,6 +2009,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2072,7 +2073,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 0b990fd814..5d11d7a946 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4929,8 +4929,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 95f1925072..620b55152d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom 
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 8ba4136220..1d59f2d90f 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -107,6 +107,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.21.3

#61Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alexander Korotkov (#59)
5 attachment(s)
Re: WIP: BRIN multi-range indexes

On Sun, Apr 05, 2020 at 08:01:50PM +0300, Alexander Korotkov wrote:

On Sun, Apr 5, 2020 at 8:00 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

...

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing to commit
them, you can count round of review on me.

I have no intention to get 0001-0003 committed. I think those changes
are beneficial on their own, but the primary reason was to support the
new opclasses (which require those changes). And those parts are not
going to make it into v13 ...

OK, no problem.
Let's do this for v14.

Hi Alexander,

Are you still interested in reviewing those patches? I'll take a look at
0001-0003 to check that your previous feedback was addressed. Do you
have any comments about 0004 / 0005, which I think are the more
interesting parts of this series?

Attached is a rebased version - I realized I forgot to include 0005 in
the last update, for some reason.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-a-20200703.patchtext/plain; charset=us-asciiDownload
From ac8e74149e8ce6c291e8c414872e1fb2903836d0 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 13 Sep 2019 18:34:39 +0200
Subject: [PATCH 1/5] Pass all keys to BRIN consistent function at once

---
 src/backend/access/brin/brin.c           | 126 ++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 164 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 116 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 293 insertions(+), 121 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 7db3ae5ee0..f1beb2eff9 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -388,6 +388,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -409,6 +412,53 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -469,7 +519,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -479,34 +529,19 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (!nkeys[attno - 1])
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
@@ -518,12 +553,43 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					 * the range as a whole, so break out of the loop as soon
 					 * as a false return value is obtained.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/* Check keys one by one */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..8968886ff5 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,103 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		matches;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = inclusion_consistent_key(bdesc, column, key, colloid);
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_BOOL(matches);
+}
+
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +376,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +437,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +458,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +477,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +512,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +522,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..1219a3a2ab 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,99 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
-	FmgrInfo   *finfo;
+	int			keyno;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = minmax_consistent_key(bdesc, column, key, colloid);
+
+		/* found non-matching key */
+		if (!matches)
+			break;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(matches);
+}
+
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +283,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 38295aca48..46cedaeee6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8076,7 +8076,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8092,7 +8092,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.25.4

0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20200703.patchtext/plain; charset=us-asciiDownload
From 1f175a68c2d5332bb86d62036bc8771b233b8791 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:05:39 +0200
Subject: [PATCH 2/5] Move IS NOT NULL checks to bringetbitmap

---
 src/backend/access/brin/brin.c           | 116 ++++++++++++++++++++---
 src/backend/access/brin/brin_inclusion.c |  62 +-----------
 src/backend/access/brin/brin_minmax.c    |  62 +-----------
 3 files changed, 109 insertions(+), 131 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index f1beb2eff9..25ae02a999 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -388,8 +388,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -414,10 +416,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,14 +444,12 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 							  keyattno - 1)->attcollation));
 
 		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -454,9 +457,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,6 +560,83 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys, and
+					 * if we're violating them. In that case we can terminate
+					 * early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					{
+						ScanKey	key = nullkeys[attno - 1][keyno];
+
+						Assert(key->sk_attno == bval->bv_attno);
+
+						/* interrupt the loop as soon as we find a mismatch */
+						if (!addrange)
+							break;
+
+						/* handle IS NULL/IS NOT NULL tests */
+						if (key->sk_flags & SK_ISNULL)
+						{
+							/* IS NULL scan key, but range has no NULLs */
+							if (key->sk_flags & SK_SEARCHNULL)
+							{
+								if (!bval->bv_allnulls && !bval->bv_hasnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * For IS NOT NULL, we can only skip ranges that are
+							 * known to have only nulls.
+							 */
+							if (key->sk_flags & SK_SEARCHNOTNULL)
+							{
+								if (bval->bv_allnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * Neither IS NULL nor IS NOT NULL was used; assume all
+							 * indexable operators are strict and thus return false
+							 * with NULL value in the scan key.
+							 */
+							addrange = false;
+						}
+					}
+
+					/*
+					 * If any of the IS [NOT] NULL keys failed, the page range as
+					 * a whole can't pass. So terminate the loop.
+					 */
+					if (!addrange)
+						break;
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed. If
+					 * there are no regular scan keys, we're done - the page range
+					 * matches. If there are regular keys, but the page range is
+					 * marked as 'all nulls' it can't possibly pass (we're assuming
+					 * the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 8968886ff5..22edc6b46f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -265,63 +265,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
 	bool		matches;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -333,9 +276,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = inclusion_consistent_key(bdesc, column, key, colloid);
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 1219a3a2ab..7a7bd21cec 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -153,63 +153,6 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	matches = true;
 
@@ -217,9 +160,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = minmax_consistent_key(bdesc, column, key, colloid);
 
-- 
2.25.4

0003-Move-processing-of-NULLs-from-BRIN-support--20200703.patchtext/plain; charset=us-asciiDownload
From f77e38ab6997775a75d0ca03fd8c2ced094dceeb Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:56:00 +0200
Subject: [PATCH 3/5] Move processing of NULLs from BRIN support functions

---
 src/backend/access/brin/brin.c           | 260 ++++++++++++++---------
 src/backend/access/brin/brin_inclusion.c |  44 +---
 src/backend/access/brin/brin_minmax.c    |  41 +---
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 169 insertions(+), 179 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 25ae02a999..8c94252567 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -177,7 +180,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -241,31 +243,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -358,6 +336,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -561,69 +540,31 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
-					 * First check if there are any IS [NOT] NULL scan keys, and
-					 * if we're violating them. In that case we can terminate
-					 * early, without invoking the support function.
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
 					 *
 					 * As there may be more keys, we can only detemine mismatch
 					 * within this loop.
 					 */
-					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
 					{
-						ScanKey	key = nullkeys[attno - 1][keyno];
-
-						Assert(key->sk_attno == bval->bv_attno);
-
-						/* interrupt the loop as soon as we find a mismatch */
-						if (!addrange)
-							break;
-
-						/* handle IS NULL/IS NOT NULL tests */
-						if (key->sk_flags & SK_ISNULL)
-						{
-							/* IS NULL scan key, but range has no NULLs */
-							if (key->sk_flags & SK_SEARCHNULL)
-							{
-								if (!bval->bv_allnulls && !bval->bv_hasnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * For IS NOT NULL, we can only skip ranges that are
-							 * known to have only nulls.
-							 */
-							if (key->sk_flags & SK_SEARCHNOTNULL)
-							{
-								if (bval->bv_allnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * Neither IS NULL nor IS NOT NULL was used; assume all
-							 * indexable operators are strict and thus return false
-							 * with NULL value in the scan key.
-							 */
-							addrange = false;
-						}
-					}
-
-					/*
-					 * If any of the IS [NOT] NULL keys failed, the page range as
-					 * a whole can't pass. So terminate the loop.
-					 */
-					if (!addrange)
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
 						break;
+					}
 
 					/*
-					 * So either there are no IS [NOT] NULL keys, or all passed. If
-					 * there are no regular scan keys, we're done - the page range
-					 * matches. If there are regular keys, but the page range is
-					 * marked as 'all nulls' it can't possibly pass (we're assuming
-					 * the operators are strict).
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
 					 */
 
 					/* No regular scan keys - page range as a whole passes. */
@@ -771,7 +712,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -800,25 +740,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1597,6 +1520,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1650,3 +1606,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 22edc6b46f..59503b6f68 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -510,37 +500,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 7a7bd21cec..8882eec12c 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -245,34 +235,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

0004-BRIN-bloom-indexes-20200703.patchtext/plain; charset=us-asciiDownload
From 9d01bf0157c545bbad5699b38afcaf6adbb820a5 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:57:59 +0200
Subject: [PATCH 4/5] BRIN bloom indexes

---
 doc/src/sgml/brin.sgml                   | 215 +++++
 doc/src/sgml/ref/create_index.sgml       |  31 +
 src/backend/access/brin/Makefile         |   1 +
 src/backend/access/brin/brin_bloom.c     | 980 +++++++++++++++++++++++
 src/include/access/brin.h                |   2 +
 src/include/access/brin_internal.h       |   4 +
 src/include/catalog/pg_amop.dat          | 165 ++++
 src/include/catalog/pg_amproc.dat        | 430 ++++++++++
 src/include/catalog/pg_opclass.dat       |  69 ++
 src/include/catalog/pg_opfamily.dat      |  36 +
 src/include/catalog/pg_proc.dat          |  20 +
 src/test/regress/expected/brin_bloom.out | 456 +++++++++++
 src/test/regress/expected/opr_sanity.out |   3 +-
 src/test/regress/expected/psql.out       |   3 +-
 src/test/regress/parallel_schedule       |   5 +
 src/test/regress/serial_schedule         |   1 +
 src/test/regress/sql/brin_bloom.sql      | 404 ++++++++++
 17 files changed, 2823 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4c5eeb875f..cc3533ef1f 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -132,6 +132,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry><literal>int8_bloom_ops</literal></entry>
+     <entry><type>bigint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int8_minmax_ops</literal></entry>
      <entry><type>bigint</type></entry>
@@ -183,6 +190,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>|&amp;&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>bytea_bloom_ops</literal></entry>
+     <entry><type>bytea</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>bytea_minmax_ops</literal></entry>
      <entry><type>bytea</type></entry>
@@ -194,6 +208,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>bpchar_bloom_ops</literal></entry>
+     <entry><type>character</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>bpchar_minmax_ops</literal></entry>
      <entry><type>character</type></entry>
@@ -205,6 +226,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>char_bloom_ops</literal></entry>
+     <entry><type>"char"</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>char_minmax_ops</literal></entry>
      <entry><type>"char"</type></entry>
@@ -216,6 +244,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>date_bloom_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>date_minmax_ops</literal></entry>
      <entry><type>date</type></entry>
@@ -227,6 +262,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float8_bloom_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float8_minmax_ops</literal></entry>
      <entry><type>double precision</type></entry>
@@ -238,6 +280,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>inet_bloom_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>inet_minmax_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -261,6 +310,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&lt;&lt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>int4_bloom_ops</literal></entry>
+     <entry><type>integer</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int4_minmax_ops</literal></entry>
      <entry><type>integer</type></entry>
@@ -272,6 +328,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>interval_bloom_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>interval_minmax_ops</literal></entry>
      <entry><type>interval</type></entry>
@@ -283,6 +346,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr_bloom_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr_minmax_ops</literal></entry>
      <entry><type>macaddr</type></entry>
@@ -294,6 +364,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr8_minmax_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
@@ -305,6 +382,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>name_bloom_ops</literal></entry>
+     <entry><type>name</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>name_minmax_ops</literal></entry>
      <entry><type>name</type></entry>
@@ -316,6 +400,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>numeric_bloom_ops</literal></entry>
+     <entry><type>numeric</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>numeric_minmax_ops</literal></entry>
      <entry><type>numeric</type></entry>
@@ -327,6 +418,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><type>pg_lsn</type></entry>
@@ -338,6 +436,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>oid_bloom_ops</literal></entry>
+     <entry><type>oid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>oid_minmax_ops</literal></entry>
      <entry><type>oid</type></entry>
@@ -369,6 +474,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;=</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float4_bloom_ops</literal></entry>
+     <entry><type>real</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float4_minmax_ops</literal></entry>
      <entry><type>real</type></entry>
@@ -380,6 +492,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>int2_bloom_ops</literal></entry>
+     <entry><type>smallint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int2_minmax_ops</literal></entry>
      <entry><type>smallint</type></entry>
@@ -391,6 +510,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>text_bloom_ops</literal></entry>
+     <entry><type>text</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>text_minmax_ops</literal></entry>
      <entry><type>text</type></entry>
@@ -413,6 +539,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamp_bloom_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamp_minmax_ops</literal></entry>
      <entry><type>timestamp without time zone</type></entry>
@@ -424,6 +557,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamptz_minmax_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
@@ -435,6 +575,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>time_bloom_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>time_minmax_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
@@ -446,6 +593,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timetz_bloom_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timetz_minmax_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
@@ -457,6 +611,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>uuid_bloom_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>uuid_minmax_ops</literal></entry>
      <entry><type>uuid</type></entry>
@@ -841,6 +1002,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 33aa64e81d..9c90d451a7 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -555,6 +555,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..b119e4e264
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,980 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i,
+			nvalues;
+	Size	len;
+	uint32 *values;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* TODO optimization: sort only the unsorted part, then merge */
+	pg_qsort(values, filter->nvalues, sizeof(uint32), cmp_uint32);
+
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (values[i] != values[nvalues - 1])
+			values[nvalues++] = values[i];
+	}
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..af6891e348 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2002,6 +2065,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2096,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2118,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2140,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2182,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2204,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2355,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2410,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2432,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2488,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2510,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2576,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 37b580883f..c24a80545a 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -770,6 +770,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -783,6 +801,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -796,6 +832,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -897,6 +951,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -910,6 +1016,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -922,6 +1046,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -984,6 +1125,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -998,6 +1178,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1012,6 +1212,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1025,6 +1245,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1059,6 +1297,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1072,6 +1330,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1179,6 +1455,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1193,6 +1525,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1207,6 +1559,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1247,6 +1619,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1260,6 +1652,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1295,6 +1705,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..16d6111ab6 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,127 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +386,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..7f733aeab1 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,86 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 46cedaeee6..5e0d29b12d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8099,6 +8099,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 27056d70d3..729d5c04c1 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2009,6 +2009,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2072,7 +2073,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7d2d6328fc..97f0c47005 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4929,8 +4929,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..74db99976a 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom 
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..abce8e180a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -107,6 +107,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.25.4

0005-BRIN-multi-range-minmax-indexes-20200703.patchtext/plain; charset=us-asciiDownload
From ba0c3abbbc456ebcc4187c7a8fdfd0fc6e0236c5 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 3 Jul 2020 02:14:59 +0200
Subject: [PATCH 5/5] BRIN multi-range minmax indexes

---
 doc/src/sgml/brin.sgml                      |  212 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2178 +++++++++++++++++++
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/transam.h                |    8 +-
 src/include/catalog/pg_amop.dat             |  545 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   65 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 ++++
 17 files changed, 4498 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index cc3533ef1f..1bf2e0dce7 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -262,6 +262,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>date_minmax_multi_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float8_bloom_ops</literal></entry>
      <entry><type>double precision</type></entry>
@@ -280,6 +291,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>inet_bloom_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -298,6 +320,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>network_inclusion_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -346,6 +379,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr_bloom_ops</literal></entry>
      <entry><type>macaddr</type></entry>
@@ -364,6 +408,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr8_bloom_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
@@ -382,6 +437,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>name_bloom_ops</literal></entry>
      <entry><type>name</type></entry>
@@ -436,6 +502,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>oid_bloom_ops</literal></entry>
      <entry><type>oid</type></entry>
@@ -557,6 +634,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamptz_bloom_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
@@ -575,6 +663,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>time_bloom_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
@@ -593,6 +692,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>time_minmax_multi_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timetz_bloom_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
@@ -611,6 +721,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>uuid_bloom_ops</literal></entry>
      <entry><type>uuid</type></entry>
@@ -629,6 +750,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
    </tbody>
   </tgroup>
  </table>
@@ -753,13 +885,13 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions using
+  them are shipped for in-core data types as appropriate.  Additional operator
+  classes can be defined by the user for other data types using equivalent
+  definitions, without having to write any source code; appropriate catalog
+  entries being declared is enough.  Note that assumptions about the semantics
+  of operator strategies are embedded in the support functions' source code.
  </para>
 
  <para>
@@ -830,6 +962,72 @@ typedef struct BrinOpcInfo
   </tgroup>
  </table>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
   To write an operator class for a complex data type which has values
   included within another type, it's possible to use the inclusion support
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 9c90d451a7..f6b8fbd1d6 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -586,6 +586,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 64.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..9b9eedbb63
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2178 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+static Ranges *range_deserialize(SerializedRanges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (attr->attlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (attr->attlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(attr->attlen > 0);
+		len += nvalues * attr->attlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized,
+				AttrNumber attno, Form_pg_attribute attr)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTLessStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count_values(cranges, ncranges) <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		FmgrInfo   *cmpFn;
+		Datum	   *values;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												   BTLessStrategyNumber);
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		column->bv_allnulls = false;
+		modified = true;
+	}
+	else
+	{
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized, attno, attr);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+	if (modified)
+	{
+		SerializedRanges *s = range_serialize(ranges, attno, attr);
+		column->bv_values[0] = PointerGetDatum(s);
+
+		/*
+		 * XXX pfree must happen after range_serialize, because the Ranges value
+		 * may reference the original serialized value.
+		 */
+		if (serialized)
+			pfree(serialized);
+	}
+
+	pfree(ranges);
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+	Form_pg_attribute attr;
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized, attno, attr);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a, attno, attr);
+	ranges_b = range_deserialize(serialized_b, attno, attr);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attno, attr));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index a91a0c7487..18a8fd77d9 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -130,14 +130,14 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  *		when the .dat files in src/include/catalog/ do not specify an OID
  *		for a catalog entry that requires one.
  *
- *		OIDS 12000-16383 are reserved for assignment during initdb
- *		using the OID generator.  (We start the generator at 12000.)
+ *		OIDS 13000-16383 are reserved for assignment during initdb
+ *		using the OID generator.  (We start the generator at 13000.)
  *
  *		OIDs beginning at 16384 are assigned from the OID generator
  *		during normal multiuser operation.  (We force the generator up to
  *		16384 as soon as we are in normal operation.)
  *
- * The choices of 8000, 10000 and 12000 are completely arbitrary, and can be
+ * The choices of 8000, 10000 and 13000 are completely arbitrary, and can be
  * moved if we run low on OIDs in any category.  Changing the macros below,
  * and updating relevant documentation (see bki.sgml and RELEASE_CHANGES),
  * should be sufficient to do this.  Moving the 16384 boundary between
@@ -151,7 +151,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index af6891e348..31ec56766a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1900,6 +1900,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1977,6 +2123,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -1999,6 +2162,23 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2065,6 +2245,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2096,6 +2342,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2118,6 +2381,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2140,6 +2420,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2204,6 +2501,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2355,6 +2669,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2410,6 +2870,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2432,6 +2909,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2488,6 +2982,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2510,6 +3021,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2576,6 +3104,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index c24a80545a..317c9475f8 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -951,6 +951,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1046,6 +1192,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1075,6 +1238,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1125,6 +1305,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1178,6 +1432,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1212,6 +1486,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1245,6 +1539,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1330,6 +1644,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1455,6 +1788,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1525,6 +2022,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1559,6 +2076,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1619,6 +2156,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1650,7 +2207,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1705,6 +2283,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 16d6111ab6..92b3a577ea 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,38 +307,59 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -345,36 +375,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -386,6 +434,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -395,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -404,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 7f733aeab1..8bbf753f65 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,24 +220,36 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -240,10 +260,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -252,12 +276,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5e0d29b12d..fa324f4608 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8082,6 +8082,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 97f0c47005..f18097a02b 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4926,12 +4926,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 74db99976a..a933db5456 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom 
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index abce8e180a..3f05a7061f 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.25.4

#62Masahiko Sawada
masahiko.sawada@2ndquadrant.com
In reply to: Tomas Vondra (#61)
Re: WIP: BRIN multi-range indexes

On Fri, 3 Jul 2020 at 09:58, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

On Sun, Apr 05, 2020 at 08:01:50PM +0300, Alexander Korotkov wrote:

On Sun, Apr 5, 2020 at 8:00 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

...

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing to commit
them, you can count round of review on me.

I have no intention to get 0001-0003 committed. I think those changes
are beneficial on their own, but the primary reason was to support the
new opclasses (which require those changes). And those parts are not
going to make it into v13 ...

OK, no problem.
Let's do this for v14.

Hi Alexander,

Are you still interested in reviewing those patches? I'll take a look at
0001-0003 to check that your previous feedback was addressed. Do you
have any comments about 0004 / 0005, which I think are the more
interesting parts of this series?

Attached is a rebased version - I realized I forgot to include 0005 in
the last update, for some reason.

I've done a quick test with this patch set. I wonder if we can improve
brin_page_items() SQL function in pageinspect as well. Currently,
brin_page_items() is hard-coded to support only normal brin indexes.
When we pass brin-bloom or brin-multi-range to that function the
binary values are shown in 'value' column but it seems not helpful for
users. For instance, here is an output of brin_page_items() with a
brin-multi-range index:

postgres(1:12801)=# select * from brin_page_items(get_raw_page('mul',
2), 'mul');
-[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------
----------------------------
itemoffset | 1
blknum | 0
attnum | 1
allnulls | f
hasnulls | f
placeholder | f
value | {\x010000001b0000002000000001000000e5700000e6700000e7700000e8700000e9700000ea700000eb700000ec700000ed700000ee700000ef
700000f0700000f1700000f2700000f3700000f4700000f5700000f6700000f7700000f8700000f9700000fa700000fb700000fc700000fd700000fe700000ff700
00000710000}

Also, I got an assertion failure when setting false_positive_rate reloption:

postgres(1:12448)=# create index blm on t using brin (c int4_bloom_ops
(false_positive_rate = 1));
TRAP: FailedAssertion("(false_positive_rate > 0) &&
(false_positive_rate < 1.0)", File: "brin_bloom.c", Line: 300)

I'll look at the code in depth and let you know if I find a problem.

Regards,

--
Masahiko Sawada http://www.2ndQuadrant.com/

PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#63Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Masahiko Sawada (#62)
Re: WIP: BRIN multi-range indexes

On Fri, Jul 10, 2020 at 06:01:58PM +0900, Masahiko Sawada wrote:

On Fri, 3 Jul 2020 at 09:58, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

On Sun, Apr 05, 2020 at 08:01:50PM +0300, Alexander Korotkov wrote:

On Sun, Apr 5, 2020 at 8:00 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

...

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing to commit
them, you can count round of review on me.

I have no intention to get 0001-0003 committed. I think those changes
are beneficial on their own, but the primary reason was to support the
new opclasses (which require those changes). And those parts are not
going to make it into v13 ...

OK, no problem.
Let's do this for v14.

Hi Alexander,

Are you still interested in reviewing those patches? I'll take a look at
0001-0003 to check that your previous feedback was addressed. Do you
have any comments about 0004 / 0005, which I think are the more
interesting parts of this series?

Attached is a rebased version - I realized I forgot to include 0005 in
the last update, for some reason.

I've done a quick test with this patch set. I wonder if we can improve
brin_page_items() SQL function in pageinspect as well. Currently,
brin_page_items() is hard-coded to support only normal brin indexes.
When we pass brin-bloom or brin-multi-range to that function the
binary values are shown in 'value' column but it seems not helpful for
users. For instance, here is an output of brin_page_items() with a
brin-multi-range index:

postgres(1:12801)=# select * from brin_page_items(get_raw_page('mul',
2), 'mul');
-[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------
----------------------------
itemoffset | 1
blknum | 0
attnum | 1
allnulls | f
hasnulls | f
placeholder | f
value | {\x010000001b0000002000000001000000e5700000e6700000e7700000e8700000e9700000ea700000eb700000ec700000ed700000ee700000ef
700000f0700000f1700000f2700000f3700000f4700000f5700000f6700000f7700000f8700000f9700000fa700000fb700000fc700000fd700000fe700000ff700
00000710000}

Hmm. I'm not sure we can do much better, without making the function
much more complicated. I mean, even with regular BRIN indexes we don't
really know if the value is plain min/max, right?

Also, I got an assertion failure when setting false_positive_rate reloption:

postgres(1:12448)=# create index blm on t using brin (c int4_bloom_ops
(false_positive_rate = 1));
TRAP: FailedAssertion("(false_positive_rate > 0) &&
(false_positive_rate < 1.0)", File: "brin_bloom.c", Line: 300)

I'll look at the code in depth and let you know if I find a problem.

Yeah, the assert should say (f_p_r <= 1.0).

But I'm not convinced we should allow values up to 1.0, really. The
f_p_r is the fraction of the table that will get matched always, so 1.0
would mean we get to scan the whole table. Seems kinda pointless. So
maybe we should cap it to something like 0.1 or so, but I agree the
value seems kinda arbitrary.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#64Sascha Kuhl
yogidabanli@gmail.com
In reply to: Tomas Vondra (#63)
Re: WIP: BRIN multi-range indexes

Tomas Vondra <tomas.vondra@2ndquadrant.com> schrieb am Fr., 10. Juli 2020,
14:09:

On Fri, Jul 10, 2020 at 06:01:58PM +0900, Masahiko Sawada wrote:

On Fri, 3 Jul 2020 at 09:58, Tomas Vondra <tomas.vondra@2ndquadrant.com>

wrote:

On Sun, Apr 05, 2020 at 08:01:50PM +0300, Alexander Korotkov wrote:

On Sun, Apr 5, 2020 at 8:00 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

...

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing to

commit

them, you can count round of review on me.

I have no intention to get 0001-0003 committed. I think those changes
are beneficial on their own, but the primary reason was to support

the

new opclasses (which require those changes). And those parts are not
going to make it into v13 ...

OK, no problem.
Let's do this for v14.

Hi Alexander,

Are you still interested in reviewing those patches? I'll take a look at
0001-0003 to check that your previous feedback was addressed. Do you
have any comments about 0004 / 0005, which I think are the more
interesting parts of this series?

Attached is a rebased version - I realized I forgot to include 0005 in
the last update, for some reason.

I've done a quick test with this patch set. I wonder if we can improve
brin_page_items() SQL function in pageinspect as well. Currently,
brin_page_items() is hard-coded to support only normal brin indexes.
When we pass brin-bloom or brin-multi-range to that function the
binary values are shown in 'value' column but it seems not helpful for
users. For instance, here is an output of brin_page_items() with a
brin-multi-range index:

postgres(1:12801)=# select * from brin_page_items(get_raw_page('mul',
2), 'mul');
-[ RECORD 1

]----------------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------------------------
----------------------------
itemoffset | 1
blknum | 0
attnum | 1
allnulls | f
hasnulls | f
placeholder | f
value |

{\x010000001b0000002000000001000000e5700000e6700000e7700000e8700000e9700000ea700000eb700000ec700000ed700000ee700000ef

700000f0700000f1700000f2700000f3700000f4700000f5700000f6700000f7700000f8700000f9700000fa700000fb700000fc700000fd700000fe700000ff700
00000710000}

Hmm. I'm not sure we can do much better, without making the function
much more complicated. I mean, even with regular BRIN indexes we don't
really know if the value is plain min/max, right?

You can be sure with the next node. The value is in can be false positiv.
The value is out is clear. You can detect the change between in and out.

Show quoted text

Also, I got an assertion failure when setting false_positive_rate

reloption:

postgres(1:12448)=# create index blm on t using brin (c int4_bloom_ops
(false_positive_rate = 1));
TRAP: FailedAssertion("(false_positive_rate > 0) &&
(false_positive_rate < 1.0)", File: "brin_bloom.c", Line: 300)

I'll look at the code in depth and let you know if I find a problem.

Yeah, the assert should say (f_p_r <= 1.0).

But I'm not convinced we should allow values up to 1.0, really. The
f_p_r is the fraction of the table that will get matched always, so 1.0
would mean we get to scan the whole table. Seems kinda pointless. So
maybe we should cap it to something like 0.1 or so, but I agree the
value seems kinda arbitrary.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#65Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Sascha Kuhl (#64)
Re: WIP: BRIN multi-range indexes

On Fri, Jul 10, 2020 at 04:44:41PM +0200, Sascha Kuhl wrote:

Tomas Vondra <tomas.vondra@2ndquadrant.com> schrieb am Fr., 10. Juli 2020,
14:09:

On Fri, Jul 10, 2020 at 06:01:58PM +0900, Masahiko Sawada wrote:

On Fri, 3 Jul 2020 at 09:58, Tomas Vondra <tomas.vondra@2ndquadrant.com>

wrote:

On Sun, Apr 05, 2020 at 08:01:50PM +0300, Alexander Korotkov wrote:

On Sun, Apr 5, 2020 at 8:00 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

...

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing to

commit

them, you can count round of review on me.

I have no intention to get 0001-0003 committed. I think those changes
are beneficial on their own, but the primary reason was to support

the

new opclasses (which require those changes). And those parts are not
going to make it into v13 ...

OK, no problem.
Let's do this for v14.

Hi Alexander,

Are you still interested in reviewing those patches? I'll take a look at
0001-0003 to check that your previous feedback was addressed. Do you
have any comments about 0004 / 0005, which I think are the more
interesting parts of this series?

Attached is a rebased version - I realized I forgot to include 0005 in
the last update, for some reason.

I've done a quick test with this patch set. I wonder if we can improve
brin_page_items() SQL function in pageinspect as well. Currently,
brin_page_items() is hard-coded to support only normal brin indexes.
When we pass brin-bloom or brin-multi-range to that function the
binary values are shown in 'value' column but it seems not helpful for
users. For instance, here is an output of brin_page_items() with a
brin-multi-range index:

postgres(1:12801)=# select * from brin_page_items(get_raw_page('mul',
2), 'mul');
-[ RECORD 1

]----------------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------------------------
----------------------------
itemoffset | 1
blknum | 0
attnum | 1
allnulls | f
hasnulls | f
placeholder | f
value |

{\x010000001b0000002000000001000000e5700000e6700000e7700000e8700000e9700000ea700000eb700000ec700000ed700000ee700000ef

700000f0700000f1700000f2700000f3700000f4700000f5700000f6700000f7700000f8700000f9700000fa700000fb700000fc700000fd700000fe700000ff700
00000710000}

Hmm. I'm not sure we can do much better, without making the function
much more complicated. I mean, even with regular BRIN indexes we don't
really know if the value is plain min/max, right?

You can be sure with the next node. The value is in can be false positiv.
The value is out is clear. You can detect the change between in and out.

I'm sorry, I don't understand what you're suggesting. How is any of this
related to false positive rate, etc?

The problem here is that while plain BRIN opclasses have fairly simple
summary that can be stored using a fixed number of simple data types
(e.g. minmax will store two values with the same data types as the
indexd column)

result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
sizeof(MinmaxOpaque));
result->oi_nstored = 2;
result->oi_opaque = (MinmaxOpaque *)
MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
result->oi_typcache[0] = result->oi_typcache[1] =
lookup_type_cache(typoid, 0);

The opclassed introduced here have somewhat more complex summary, stored
as a single bytea value - which is what gets printed by brin_page_items.

To print something easier to read (for humans) we'd either have to teach
brin_page_items about the diffrent opclasses (multi-range, bloom) end
how to parse the summary bytea, or we'd have to extend the opclasses
with a function formatting the summary. Or rework how the summary is
stored, but that seems like the worst option.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#66Sascha Kuhl
yogidabanli@gmail.com
In reply to: Tomas Vondra (#65)
Re: WIP: BRIN multi-range indexes

Tomas Vondra <tomas.vondra@2ndquadrant.com> schrieb am Sa., 11. Juli 2020,
13:24:

On Fri, Jul 10, 2020 at 04:44:41PM +0200, Sascha Kuhl wrote:

Tomas Vondra <tomas.vondra@2ndquadrant.com> schrieb am Fr., 10. Juli

2020,

14:09:

On Fri, Jul 10, 2020 at 06:01:58PM +0900, Masahiko Sawada wrote:

On Fri, 3 Jul 2020 at 09:58, Tomas Vondra <

tomas.vondra@2ndquadrant.com>

wrote:

On Sun, Apr 05, 2020 at 08:01:50PM +0300, Alexander Korotkov wrote:

On Sun, Apr 5, 2020 at 8:00 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

...

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing to

commit

them, you can count round of review on me.

I have no intention to get 0001-0003 committed. I think those

changes

are beneficial on their own, but the primary reason was to support

the

new opclasses (which require those changes). And those parts are

not

going to make it into v13 ...

OK, no problem.
Let's do this for v14.

Hi Alexander,

Are you still interested in reviewing those patches? I'll take a

look at

0001-0003 to check that your previous feedback was addressed. Do you
have any comments about 0004 / 0005, which I think are the more
interesting parts of this series?

Attached is a rebased version - I realized I forgot to include 0005

in

the last update, for some reason.

I've done a quick test with this patch set. I wonder if we can improve
brin_page_items() SQL function in pageinspect as well. Currently,
brin_page_items() is hard-coded to support only normal brin indexes.
When we pass brin-bloom or brin-multi-range to that function the
binary values are shown in 'value' column but it seems not helpful for
users. For instance, here is an output of brin_page_items() with a
brin-multi-range index:

postgres(1:12801)=# select * from brin_page_items(get_raw_page('mul',
2), 'mul');
-[ RECORD 1

]----------------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------------------------

----------------------------
itemoffset | 1
blknum | 0
attnum | 1
allnulls | f
hasnulls | f
placeholder | f
value |

{\x010000001b0000002000000001000000e5700000e6700000e7700000e8700000e9700000ea700000eb700000ec700000ed700000ee700000ef

700000f0700000f1700000f2700000f3700000f4700000f5700000f6700000f7700000f8700000f9700000fa700000fb700000fc700000fd700000fe700000ff700

00000710000}

Hmm. I'm not sure we can do much better, without making the function
much more complicated. I mean, even with regular BRIN indexes we don't
really know if the value is plain min/max, right?

You can be sure with the next node. The value is in can be false positiv.
The value is out is clear. You can detect the change between in and out.

I'm sorry, I don't understand what you're suggesting. How is any of this
related to false positive rate, etc?

Hi,

You check by the bloom filter if a value you're searching is part of the
node, right?

In case, the value is in the bloom filter you could be mistaken, because
another value could have the same hash profile, no?

However if the value is out, the filter can not react. You can be sure that
the value is out.

If you looking for a range or many ranges of values, you traverse many
nodes. By knowing the value is out, you can state a clear set of nodes that
form the range. However the border is somehow unsharp because of the false
positives.

I am not sure if we write about the same. Please confirm, this can be
needed. Please.

I will try to understand what you write. Interesting

Sascha

Show quoted text

The problem here is that while plain BRIN opclasses have fairly simple
summary that can be stored using a fixed number of simple data types
(e.g. minmax will store two values with the same data types as the
indexd column)

result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
sizeof(MinmaxOpaque));
result->oi_nstored = 2;
result->oi_opaque = (MinmaxOpaque *)
MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
result->oi_typcache[0] = result->oi_typcache[1] =
lookup_type_cache(typoid, 0);

The opclassed introduced here have somewhat more complex summary, stored
as a single bytea value - which is what gets printed by brin_page_items.

To print something easier to read (for humans) we'd either have to teach
brin_page_items about the diffrent opclasses (multi-range, bloom) end
how to parse the summary bytea, or we'd have to extend the opclasses
with a function formatting the summary. Or rework how the summary is
stored, but that seems like the worst option.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#67Sascha Kuhl
yogidabanli@gmail.com
In reply to: Sascha Kuhl (#66)
Re: WIP: BRIN multi-range indexes

Sorry, my topic is different

Sascha Kuhl <yogidabanli@gmail.com> schrieb am Sa., 11. Juli 2020, 15:32:

Show quoted text

Tomas Vondra <tomas.vondra@2ndquadrant.com> schrieb am Sa., 11. Juli
2020, 13:24:

On Fri, Jul 10, 2020 at 04:44:41PM +0200, Sascha Kuhl wrote:

Tomas Vondra <tomas.vondra@2ndquadrant.com> schrieb am Fr., 10. Juli

2020,

14:09:

On Fri, Jul 10, 2020 at 06:01:58PM +0900, Masahiko Sawada wrote:

On Fri, 3 Jul 2020 at 09:58, Tomas Vondra <

tomas.vondra@2ndquadrant.com>

wrote:

On Sun, Apr 05, 2020 at 08:01:50PM +0300, Alexander Korotkov wrote:

On Sun, Apr 5, 2020 at 8:00 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

...

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing to

commit

them, you can count round of review on me.

I have no intention to get 0001-0003 committed. I think those

changes

are beneficial on their own, but the primary reason was to

support

the

new opclasses (which require those changes). And those parts are

not

going to make it into v13 ...

OK, no problem.
Let's do this for v14.

Hi Alexander,

Are you still interested in reviewing those patches? I'll take a

look at

0001-0003 to check that your previous feedback was addressed. Do you
have any comments about 0004 / 0005, which I think are the more
interesting parts of this series?

Attached is a rebased version - I realized I forgot to include 0005

in

the last update, for some reason.

I've done a quick test with this patch set. I wonder if we can improve
brin_page_items() SQL function in pageinspect as well. Currently,
brin_page_items() is hard-coded to support only normal brin indexes.
When we pass brin-bloom or brin-multi-range to that function the
binary values are shown in 'value' column but it seems not helpful for
users. For instance, here is an output of brin_page_items() with a
brin-multi-range index:

postgres(1:12801)=# select * from brin_page_items(get_raw_page('mul',
2), 'mul');
-[ RECORD 1

]----------------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------------------------

----------------------------
itemoffset | 1
blknum | 0
attnum | 1
allnulls | f
hasnulls | f
placeholder | f
value |

{\x010000001b0000002000000001000000e5700000e6700000e7700000e8700000e9700000ea700000eb700000ec700000ed700000ee700000ef

700000f0700000f1700000f2700000f3700000f4700000f5700000f6700000f7700000f8700000f9700000fa700000fb700000fc700000fd700000fe700000ff700

00000710000}

Hmm. I'm not sure we can do much better, without making the function
much more complicated. I mean, even with regular BRIN indexes we don't
really know if the value is plain min/max, right?

You can be sure with the next node. The value is in can be false positiv.
The value is out is clear. You can detect the change between in and out.

I'm sorry, I don't understand what you're suggesting. How is any of this
related to false positive rate, etc?

Hi,

You check by the bloom filter if a value you're searching is part of the
node, right?

In case, the value is in the bloom filter you could be mistaken, because
another value could have the same hash profile, no?

However if the value is out, the filter can not react. You can be sure
that the value is out.

If you looking for a range or many ranges of values, you traverse many
nodes. By knowing the value is out, you can state a clear set of nodes that
form the range. However the border is somehow unsharp because of the false
positives.

I am not sure if we write about the same. Please confirm, this can be
needed. Please.

I will try to understand what you write. Interesting

Sascha

The problem here is that while plain BRIN opclasses have fairly simple
summary that can be stored using a fixed number of simple data types
(e.g. minmax will store two values with the same data types as the
indexd column)

result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
sizeof(MinmaxOpaque));
result->oi_nstored = 2;
result->oi_opaque = (MinmaxOpaque *)
MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
result->oi_typcache[0] = result->oi_typcache[1] =
lookup_type_cache(typoid, 0);

The opclassed introduced here have somewhat more complex summary, stored
as a single bytea value - which is what gets printed by brin_page_items.

To print something easier to read (for humans) we'd either have to teach
brin_page_items about the diffrent opclasses (multi-range, bloom) end
how to parse the summary bytea, or we'd have to extend the opclasses
with a function formatting the summary. Or rework how the summary is
stored, but that seems like the worst option.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#68Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Sascha Kuhl (#66)
Re: WIP: BRIN multi-range indexes

On Sat, Jul 11, 2020 at 03:32:43PM +0200, Sascha Kuhl wrote:

Tomas Vondra <tomas.vondra@2ndquadrant.com> schrieb am Sa., 11. Juli 2020,
13:24:

On Fri, Jul 10, 2020 at 04:44:41PM +0200, Sascha Kuhl wrote:

Tomas Vondra <tomas.vondra@2ndquadrant.com> schrieb am Fr., 10. Juli

2020,

14:09:

On Fri, Jul 10, 2020 at 06:01:58PM +0900, Masahiko Sawada wrote:

On Fri, 3 Jul 2020 at 09:58, Tomas Vondra <

tomas.vondra@2ndquadrant.com>

wrote:

On Sun, Apr 05, 2020 at 08:01:50PM +0300, Alexander Korotkov wrote:

On Sun, Apr 5, 2020 at 8:00 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

...

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing to

commit

them, you can count round of review on me.

I have no intention to get 0001-0003 committed. I think those

changes

are beneficial on their own, but the primary reason was to support

the

new opclasses (which require those changes). And those parts are

not

going to make it into v13 ...

OK, no problem.
Let's do this for v14.

Hi Alexander,

Are you still interested in reviewing those patches? I'll take a

look at

0001-0003 to check that your previous feedback was addressed. Do you
have any comments about 0004 / 0005, which I think are the more
interesting parts of this series?

Attached is a rebased version - I realized I forgot to include 0005

in

the last update, for some reason.

I've done a quick test with this patch set. I wonder if we can improve
brin_page_items() SQL function in pageinspect as well. Currently,
brin_page_items() is hard-coded to support only normal brin indexes.
When we pass brin-bloom or brin-multi-range to that function the
binary values are shown in 'value' column but it seems not helpful for
users. For instance, here is an output of brin_page_items() with a
brin-multi-range index:

postgres(1:12801)=# select * from brin_page_items(get_raw_page('mul',
2), 'mul');
-[ RECORD 1

]----------------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------------------------

----------------------------
itemoffset | 1
blknum | 0
attnum | 1
allnulls | f
hasnulls | f
placeholder | f
value |

{\x010000001b0000002000000001000000e5700000e6700000e7700000e8700000e9700000ea700000eb700000ec700000ed700000ee700000ef

700000f0700000f1700000f2700000f3700000f4700000f5700000f6700000f7700000f8700000f9700000fa700000fb700000fc700000fd700000fe700000ff700

00000710000}

Hmm. I'm not sure we can do much better, without making the function
much more complicated. I mean, even with regular BRIN indexes we don't
really know if the value is plain min/max, right?

You can be sure with the next node. The value is in can be false positiv.
The value is out is clear. You can detect the change between in and out.

I'm sorry, I don't understand what you're suggesting. How is any of this
related to false positive rate, etc?

Hi,

You check by the bloom filter if a value you're searching is part of the
node, right?

In case, the value is in the bloom filter you could be mistaken, because
another value could have the same hash profile, no?

However if the value is out, the filter can not react. You can be sure that
the value is out.

If you looking for a range or many ranges of values, you traverse many
nodes. By knowing the value is out, you can state a clear set of nodes that
form the range. However the border is somehow unsharp because of the false
positives.

I am not sure if we write about the same. Please confirm, this can be
needed. Please.

Probably not. Masahiko-san pointed out that pageinspect (which also has
a function to print pages from a BRIN index) does not understand the
summary of the new opclasses and just prints the bytea verbatim.

That has nothing to do with inspecting the bloom filter, or anything
like that. So I think there's some confusion ...

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#69Sascha Kuhl
yogidabanli@gmail.com
In reply to: Tomas Vondra (#68)
Re: WIP: BRIN multi-range indexes

Thanks, I see there is some understanding, though.

Tomas Vondra <tomas.vondra@2ndquadrant.com> schrieb am So., 12. Juli 2020,
01:30:

Show quoted text

On Sat, Jul 11, 2020 at 03:32:43PM +0200, Sascha Kuhl wrote:

Tomas Vondra <tomas.vondra@2ndquadrant.com> schrieb am Sa., 11. Juli

2020,

13:24:

On Fri, Jul 10, 2020 at 04:44:41PM +0200, Sascha Kuhl wrote:

Tomas Vondra <tomas.vondra@2ndquadrant.com> schrieb am Fr., 10. Juli

2020,

14:09:

On Fri, Jul 10, 2020 at 06:01:58PM +0900, Masahiko Sawada wrote:

On Fri, 3 Jul 2020 at 09:58, Tomas Vondra <

tomas.vondra@2ndquadrant.com>

wrote:

On Sun, Apr 05, 2020 at 08:01:50PM +0300, Alexander Korotkov

wrote:

On Sun, Apr 5, 2020 at 8:00 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

...

Assuming we're not going to get 0001-0003 into v13, I'm not so
inclined to rush on these three as well. But you're willing

to

commit

them, you can count round of review on me.

I have no intention to get 0001-0003 committed. I think those

changes

are beneficial on their own, but the primary reason was to

support

the

new opclasses (which require those changes). And those parts

are

not

going to make it into v13 ...

OK, no problem.
Let's do this for v14.

Hi Alexander,

Are you still interested in reviewing those patches? I'll take a

look at

0001-0003 to check that your previous feedback was addressed. Do

you

have any comments about 0004 / 0005, which I think are the more
interesting parts of this series?

Attached is a rebased version - I realized I forgot to include

0005

in

the last update, for some reason.

I've done a quick test with this patch set. I wonder if we can

improve

brin_page_items() SQL function in pageinspect as well. Currently,
brin_page_items() is hard-coded to support only normal brin indexes.
When we pass brin-bloom or brin-multi-range to that function the
binary values are shown in 'value' column but it seems not helpful

for

users. For instance, here is an output of brin_page_items() with a
brin-multi-range index:

postgres(1:12801)=# select * from

brin_page_items(get_raw_page('mul',

2), 'mul');
-[ RECORD 1

]----------------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------------------------

----------------------------
itemoffset | 1
blknum | 0
attnum | 1
allnulls | f
hasnulls | f
placeholder | f
value |

{\x010000001b0000002000000001000000e5700000e6700000e7700000e8700000e9700000ea700000eb700000ec700000ed700000ee700000ef

700000f0700000f1700000f2700000f3700000f4700000f5700000f6700000f7700000f8700000f9700000fa700000fb700000fc700000fd700000fe700000ff700

00000710000}

Hmm. I'm not sure we can do much better, without making the function
much more complicated. I mean, even with regular BRIN indexes we

don't

really know if the value is plain min/max, right?

You can be sure with the next node. The value is in can be false

positiv.

The value is out is clear. You can detect the change between in and

out.

I'm sorry, I don't understand what you're suggesting. How is any of this
related to false positive rate, etc?

Hi,

You check by the bloom filter if a value you're searching is part of the
node, right?

In case, the value is in the bloom filter you could be mistaken, because
another value could have the same hash profile, no?

However if the value is out, the filter can not react. You can be sure

that

the value is out.

If you looking for a range or many ranges of values, you traverse many
nodes. By knowing the value is out, you can state a clear set of nodes

that

form the range. However the border is somehow unsharp because of the false
positives.

I am not sure if we write about the same. Please confirm, this can be
needed. Please.

Probably not. Masahiko-san pointed out that pageinspect (which also has
a function to print pages from a BRIN index) does not understand the
summary of the new opclasses and just prints the bytea verbatim.

That has nothing to do with inspecting the bloom filter, or anything
like that. So I think there's some confusion ...

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#70Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Tomas Vondra (#63)
Re: WIP: BRIN multi-range indexes

On 2020-Jul-10, Tomas Vondra wrote:

postgres(1:12801)=# select * from brin_page_items(get_raw_page('mul',
2), 'mul');
-[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------
----------------------------
itemoffset | 1
blknum | 0
attnum | 1
allnulls | f
hasnulls | f
placeholder | f
value | {\x010000001b0000002000000001000000e5700000e6700000e7700000e8700000e9700000ea700000eb700000ec700000ed700000ee700000ef
700000f0700000f1700000f2700000f3700000f4700000f5700000f6700000f7700000f8700000f9700000fa700000fb700000fc700000fd700000fe700000ff700
00000710000}

Hmm. I'm not sure we can do much better, without making the function
much more complicated. I mean, even with regular BRIN indexes we don't
really know if the value is plain min/max, right?

Maybe we can try to handle this with some other function that interprets
the bytea in 'value' and returns a user-readable text. I think it'd
have to be a superuser-only function, because otherwise you could easily
cause a crash by passing a value of a different opclass. But since this
seems a developer-only thing, that restriction seems fine to me.

(I don't know what's a good way to represent a bloom filter, mind.)

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#71Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alvaro Herrera (#70)
Re: WIP: BRIN multi-range indexes

On Sun, Jul 12, 2020 at 07:58:54PM -0400, Alvaro Herrera wrote:

On 2020-Jul-10, Tomas Vondra wrote:

postgres(1:12801)=# select * from brin_page_items(get_raw_page('mul',
2), 'mul');
-[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------
----------------------------
itemoffset | 1
blknum | 0
attnum | 1
allnulls | f
hasnulls | f
placeholder | f
value | {\x010000001b0000002000000001000000e5700000e6700000e7700000e8700000e9700000ea700000eb700000ec700000ed700000ee700000ef
700000f0700000f1700000f2700000f3700000f4700000f5700000f6700000f7700000f8700000f9700000fa700000fb700000fc700000fd700000fe700000ff700
00000710000}

Hmm. I'm not sure we can do much better, without making the function
much more complicated. I mean, even with regular BRIN indexes we don't
really know if the value is plain min/max, right?

Maybe we can try to handle this with some other function that interprets
the bytea in 'value' and returns a user-readable text. I think it'd
have to be a superuser-only function, because otherwise you could easily
cause a crash by passing a value of a different opclass. But since this
seems a developer-only thing, that restriction seems fine to me.

Ummm, I disagree a superuser check is sufficient protection from a
segfault or similar issues. If we really want to print something nicer,
I'd say it needs to be a special function in the BRIN opclass.

(I don't know what's a good way to represent a bloom filter, mind.)

Me neither, but I guess we could print either some stats (size, number
of bits set, etc.) and/or then the bitmap.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#72Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Tomas Vondra (#71)
Re: WIP: BRIN multi-range indexes

On 2020-Jul-13, Tomas Vondra wrote:

On Sun, Jul 12, 2020 at 07:58:54PM -0400, Alvaro Herrera wrote:

Maybe we can try to handle this with some other function that interprets
the bytea in 'value' and returns a user-readable text. I think it'd
have to be a superuser-only function, because otherwise you could easily
cause a crash by passing a value of a different opclass. But since this
seems a developer-only thing, that restriction seems fine to me.

Ummm, I disagree a superuser check is sufficient protection from a
segfault or similar issues.

My POV there is that it's the user's responsibility to call the right
function; and if they fail to do so, it's their fault. I agree it's not
ideal, but frankly these pageinspect things are not critical to get 100%
user-friendly.

If we really want to print something nicer, I'd say it needs to be a
special function in the BRIN opclass.

If that can be done, then +1. We just need to ensure that the function
knows and can verify the type of index that the value comes from. I
guess we can pass the index OID so that it can extract the opclass from
catalogs to verify.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#73Masahiko Sawada
masahiko.sawada@2ndquadrant.com
In reply to: Alvaro Herrera (#72)
Re: WIP: BRIN multi-range indexes

On Mon, 13 Jul 2020 at 09:33, Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

On 2020-Jul-13, Tomas Vondra wrote:

On Sun, Jul 12, 2020 at 07:58:54PM -0400, Alvaro Herrera wrote:

Maybe we can try to handle this with some other function that interprets
the bytea in 'value' and returns a user-readable text. I think it'd
have to be a superuser-only function, because otherwise you could easily
cause a crash by passing a value of a different opclass. But since this
seems a developer-only thing, that restriction seems fine to me.

Ummm, I disagree a superuser check is sufficient protection from a
segfault or similar issues.

My POV there is that it's the user's responsibility to call the right
function; and if they fail to do so, it's their fault. I agree it's not
ideal, but frankly these pageinspect things are not critical to get 100%
user-friendly.

If we really want to print something nicer, I'd say it needs to be a
special function in the BRIN opclass.

If that can be done, then +1. We just need to ensure that the function
knows and can verify the type of index that the value comes from. I
guess we can pass the index OID so that it can extract the opclass from
catalogs to verify.

+1 from me, too. Perhaps we can have it as optional. If a BRIN opclass
doesn't have it, the 'values' can be null.

Regards,

--
Masahiko Sawada http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#74Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Masahiko Sawada (#73)
Re: WIP: BRIN multi-range indexes

On Mon, Jul 13, 2020 at 02:54:56PM +0900, Masahiko Sawada wrote:

On Mon, 13 Jul 2020 at 09:33, Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

On 2020-Jul-13, Tomas Vondra wrote:

On Sun, Jul 12, 2020 at 07:58:54PM -0400, Alvaro Herrera wrote:

Maybe we can try to handle this with some other function that interprets
the bytea in 'value' and returns a user-readable text. I think it'd
have to be a superuser-only function, because otherwise you could easily
cause a crash by passing a value of a different opclass. But since this
seems a developer-only thing, that restriction seems fine to me.

Ummm, I disagree a superuser check is sufficient protection from a
segfault or similar issues.

My POV there is that it's the user's responsibility to call the right
function; and if they fail to do so, it's their fault. I agree it's not
ideal, but frankly these pageinspect things are not critical to get 100%
user-friendly.

If we really want to print something nicer, I'd say it needs to be a
special function in the BRIN opclass.

If that can be done, then +1. We just need to ensure that the function
knows and can verify the type of index that the value comes from. I
guess we can pass the index OID so that it can extract the opclass from
catalogs to verify.

+1 from me, too. Perhaps we can have it as optional. If a BRIN opclass
doesn't have it, the 'values' can be null.

I'd say that if the opclass does not have it, then we should print the
bytea value (or whatever the opclass uses to store the summary) using
the type functions.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#75Alexander Korotkov
aekorotkov@gmail.com
In reply to: Tomas Vondra (#74)
Re: WIP: BRIN multi-range indexes

On Mon, Jul 13, 2020 at 5:59 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

If we really want to print something nicer, I'd say it needs to be a
special function in the BRIN opclass.

If that can be done, then +1. We just need to ensure that the function
knows and can verify the type of index that the value comes from. I
guess we can pass the index OID so that it can extract the opclass from
catalogs to verify.

+1 from me, too. Perhaps we can have it as optional. If a BRIN opclass
doesn't have it, the 'values' can be null.

I'd say that if the opclass does not have it, then we should print the
bytea value (or whatever the opclass uses to store the summary) using
the type functions.

I've read the recent messages in this thread and I'd like to share my thoughts.

I think the way brin_page_items() displays values is not really
generic. It uses a range-like textual representation of an array of
values, while that array doesn't necessarily have range semantics.

However, I think it's good that brin_page_items() uses a type output
function to display values. So, it's not necessary to introduce a new
BRIN opclass function in order to get values displayed in a
human-readable way. Instead, we could just make a standard of BRIN
value to be human readable. I see at least two possibilities for
that.
1. Use standard container data-types to represent BRIN values. For
instance we could use an array of ranges instead of bytea for
multirange. Not about how convenient/performant it would be.
2. Introduce new data-type to represent values in BRIN index. And for
that type we can define output function with user-readable output. We
did similar things for GiST. For instance, pg_trgm defines gtrgm
type, which has no input and no output. But for BRIN opclass we can
define type with just output.

BTW, I've applied the patchset to the current master, but I got a lot
of duplicate oids. Could you please resolve these conflicts. I think
it would be good to use high oid numbers to evade conflicts during
development/review, and rely on committer to set final oids (as
discussed in [1]).

Links
1. /messages/by-id/CAH2-WzmMTGMcPuph4OvsO7Ykut0AOCF_i-=eaochT0dd2BN9CQ@mail.gmail.com

------
Regards,
Alexander Korotkov

#76Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alexander Korotkov (#75)
Re: WIP: BRIN multi-range indexes

On Wed, Jul 15, 2020 at 05:34:05AM +0300, Alexander Korotkov wrote:

On Mon, Jul 13, 2020 at 5:59 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

If we really want to print something nicer, I'd say it needs to be a
special function in the BRIN opclass.

If that can be done, then +1. We just need to ensure that the function
knows and can verify the type of index that the value comes from. I
guess we can pass the index OID so that it can extract the opclass from
catalogs to verify.

+1 from me, too. Perhaps we can have it as optional. If a BRIN opclass
doesn't have it, the 'values' can be null.

I'd say that if the opclass does not have it, then we should print the
bytea value (or whatever the opclass uses to store the summary) using
the type functions.

I've read the recent messages in this thread and I'd like to share my thoughts.

I think the way brin_page_items() displays values is not really
generic. It uses a range-like textual representation of an array of
values, while that array doesn't necessarily have range semantics.

However, I think it's good that brin_page_items() uses a type output
function to display values. So, it's not necessary to introduce a new
BRIN opclass function in order to get values displayed in a
human-readable way. Instead, we could just make a standard of BRIN
value to be human readable. I see at least two possibilities for
that.
1. Use standard container data-types to represent BRIN values. For
instance we could use an array of ranges instead of bytea for
multirange. Not about how convenient/performant it would be.
2. Introduce new data-type to represent values in BRIN index. And for
that type we can define output function with user-readable output. We
did similar things for GiST. For instance, pg_trgm defines gtrgm
type, which has no input and no output. But for BRIN opclass we can
define type with just output.

I think there's a number of weak points in this approach.

Firstly, it assumes the summaries can be represented as arrays of
built-in types, which I'm not really sure about. It clearly is not true
for the bloom opclasses, for example. But even for minmax oclasses it's
going to be tricky because the ranges may be on different data types so
presumably we'd need somewhat nested data structure.

Moreover, multi-minmax summary contains either points or intervals,
which requires additional fields/flags to indicate that. That further
complicates the things ...

maybe we could decompose that into separate arrays or something, but
honestly it seems somewhat premature - there are far more important
aspects to discuss, I think (e.g. how the ranges are built/merged in
multi-minmax, or whether bloom opclasses are useful at all).

BTW, I've applied the patchset to the current master, but I got a lot
of duplicate oids. Could you please resolve these conflicts. I think
it would be good to use high oid numbers to evade conflicts during
development/review, and rely on committer to set final oids (as
discussed in [1]).

Links
1. /messages/by-id/CAH2-WzmMTGMcPuph4OvsO7Ykut0AOCF_i-=eaochT0dd2BN9CQ@mail.gmail.com

Did you use the patchset from 2020/07/03? I don't get any duplicate OIDs
with it, and it's already using quite high OIDs (part 4 uses >= 8000,
part 5 uses >= 9000).

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#77Alexander Korotkov
aekorotkov@gmail.com
In reply to: Tomas Vondra (#76)
Re: WIP: BRIN multi-range indexes

Hi, Tomas!

Sorry for the late reply.

On Sun, Jul 19, 2020 at 6:19 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

I think there's a number of weak points in this approach.

Firstly, it assumes the summaries can be represented as arrays of
built-in types, which I'm not really sure about. It clearly is not true
for the bloom opclasses, for example. But even for minmax oclasses it's
going to be tricky because the ranges may be on different data types so
presumably we'd need somewhat nested data structure.

Moreover, multi-minmax summary contains either points or intervals,
which requires additional fields/flags to indicate that. That further
complicates the things ...

maybe we could decompose that into separate arrays or something, but
honestly it seems somewhat premature - there are far more important
aspects to discuss, I think (e.g. how the ranges are built/merged in
multi-minmax, or whether bloom opclasses are useful at all).

I see. But there is at least a second option to introduce a new
datatype with just an output function. In the similar way
gist/tsvector_ops uses gtsvector key type. I think it would be more
transparent than using just bytea. Also, this is the way we already
use in the core.

BTW, I've applied the patchset to the current master, but I got a lot
of duplicate oids. Could you please resolve these conflicts. I think
it would be good to use high oid numbers to evade conflicts during
development/review, and rely on committer to set final oids (as
discussed in [1]).

Links
1. /messages/by-id/CAH2-WzmMTGMcPuph4OvsO7Ykut0AOCF_i-=eaochT0dd2BN9CQ@mail.gmail.com

Did you use the patchset from 2020/07/03? I don't get any duplicate OIDs
with it, and it's already using quite high OIDs (part 4 uses >= 8000,
part 5 uses >= 9000).

Yep, it appears that I was using the wrong version of patchset.
Patchset from 2020/07/03 works good on the current master.

------
Regards,
Alexander Korotkov

#78Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alexander Korotkov (#77)
Re: WIP: BRIN multi-range indexes

On Tue, Aug 04, 2020 at 05:36:51PM +0300, Alexander Korotkov wrote:

Hi, Tomas!

Sorry for the late reply.

On Sun, Jul 19, 2020 at 6:19 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

I think there's a number of weak points in this approach.

Firstly, it assumes the summaries can be represented as arrays of
built-in types, which I'm not really sure about. It clearly is not true
for the bloom opclasses, for example. But even for minmax oclasses it's
going to be tricky because the ranges may be on different data types so
presumably we'd need somewhat nested data structure.

Moreover, multi-minmax summary contains either points or intervals,
which requires additional fields/flags to indicate that. That further
complicates the things ...

maybe we could decompose that into separate arrays or something, but
honestly it seems somewhat premature - there are far more important
aspects to discuss, I think (e.g. how the ranges are built/merged in
multi-minmax, or whether bloom opclasses are useful at all).

I see. But there is at least a second option to introduce a new
datatype with just an output function. In the similar way
gist/tsvector_ops uses gtsvector key type. I think it would be more
transparent than using just bytea. Also, this is the way we already
use in the core.

So you're proposing to have a new data types "brin_minmax_multi_summary"
and "brin_bloom_summary" (or some other names), with output functions
printing something nicer? I suppose that could work, and we could even
add pageinspect functions returning the value as raw bytea.

Good idea!

BTW, I've applied the patchset to the current master, but I got a lot
of duplicate oids. Could you please resolve these conflicts. I think
it would be good to use high oid numbers to evade conflicts during
development/review, and rely on committer to set final oids (as
discussed in [1]).

Links
1. /messages/by-id/CAH2-WzmMTGMcPuph4OvsO7Ykut0AOCF_i-=eaochT0dd2BN9CQ@mail.gmail.com

Did you use the patchset from 2020/07/03? I don't get any duplicate OIDs
with it, and it's already using quite high OIDs (part 4 uses >= 8000,
part 5 uses >= 9000).

Yep, it appears that I was using the wrong version of patchset.
Patchset from 2020/07/03 works good on the current master.

OK, good.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#79Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#78)
8 attachment(s)
Re: WIP: BRIN multi-range indexes

On Tue, Aug 04, 2020 at 05:17:43PM +0200, Tomas Vondra wrote:

On Tue, Aug 04, 2020 at 05:36:51PM +0300, Alexander Korotkov wrote:

Hi, Tomas!

Sorry for the late reply.

On Sun, Jul 19, 2020 at 6:19 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

I think there's a number of weak points in this approach.

Firstly, it assumes the summaries can be represented as arrays of
built-in types, which I'm not really sure about. It clearly is not true
for the bloom opclasses, for example. But even for minmax oclasses it's
going to be tricky because the ranges may be on different data types so
presumably we'd need somewhat nested data structure.

Moreover, multi-minmax summary contains either points or intervals,
which requires additional fields/flags to indicate that. That further
complicates the things ...

maybe we could decompose that into separate arrays or something, but
honestly it seems somewhat premature - there are far more important
aspects to discuss, I think (e.g. how the ranges are built/merged in
multi-minmax, or whether bloom opclasses are useful at all).

I see. But there is at least a second option to introduce a new
datatype with just an output function. In the similar way
gist/tsvector_ops uses gtsvector key type. I think it would be more
transparent than using just bytea. Also, this is the way we already
use in the core.

So you're proposing to have a new data types "brin_minmax_multi_summary"
and "brin_bloom_summary" (or some other names), with output functions
printing something nicer? I suppose that could work, and we could even
add pageinspect functions returning the value as raw bytea.

Good idea!

Attached is an updated version of the patch series, implementing this.
Adding the extra data types was fairly simple, because both bloom and
minmax-multi indexes already used "struct as varlena" approach, so all
that needed was a bunch of in/out functions and catalog records.

I've left the changes in separate patches for clarity, ultimately it'll
get merged into the other parts.

This reminded me that the current costing may not quite work, because
it depends on how well the index is correlated to the table. That may
be OK for minmax-multi in most cases, but for bloom it makes almost no
sense - correlation does not really matter for bloom filters, what
matters is the number of values in each range.

Consider this example:

create table t (a int);

insert into t select x from (
select (i/10) as x from generate_series(1,10000000) s(i)
order by random()
) foo;

create index on t using brin(
a int4_bloom_ops(n_distinct_per_range=6000,
false_positive_rate=0.05))
with (pages_per_range = 16);

vacuum analyze t;

test=# explain analyze select * from t where a = 10000;
QUERY PLAN
-----------------------------------------------------------------------------------------------------
Seq Scan on t (cost=0.00..169247.71 rows=10 width=4) (actual time=38.088..513.654 rows=10 loops=1)
Filter: (a = 10000)
Rows Removed by Filter: 9999990
Planning Time: 0.060 ms
Execution Time: 513.719 ms
(5 rows)

test=# set enable_seqscan = off;
SET
test=# explain analyze select * from t where a = 10000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on t (cost=5553.07..174800.78 rows=10 width=4) (actual time=7.790..27.585 rows=10 loops=1)
Recheck Cond: (a = 10000)
Rows Removed by Index Recheck: 224182
Heap Blocks: lossy=992
-> Bitmap Index Scan on t_a_idx (cost=0.00..5553.06 rows=9999977 width=0) (actual time=7.006..7.007 rows=9920 loops=1)
Index Cond: (a = 10000)
Planning Time: 0.052 ms
Execution Time: 27.658 ms
(8 rows)

Clearly, the main problem is in brincostestimate relying on correlation
to tweak the selectivity estimates, leading to an estimate of almost the
whole table, when in practice we only scan a tiny fraction.

Part 0008 is an experimental tweaks the logic to ignore correlation for
bloom and minmax-multi opclasses, producing this plan:

test=# explain analyze select * from t where a = 10000;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on t (cost=5542.01..16562.95 rows=10 width=4) (actual time=12.013..34.705 rows=10 loops=1)
Recheck Cond: (a = 10000)
Rows Removed by Index Recheck: 224182
Heap Blocks: lossy=992
-> Bitmap Index Scan on t_a_idx (cost=0.00..5542.00 rows=3615 width=0) (actual time=11.108..11.109 rows=9920 loops=1)
Index Cond: (a = 10000)
Planning Time: 0.386 ms
Execution Time: 34.778 ms
(8 rows)

which is way closer to reality, of course. I'm not entirely sure it
behaves correctly for multi-column BRIN indexes, but I think as a PoC
it's sufficient.

For bloom, I think we can be a bit smarter - we could use the false
positive rate as the "minimum expected selectivity" or something like
that. After all, the false positive rate essentially means "Given a
random value, what's the chance that a bloom filter matches?" So given a
table with N ranges, we expect about (N * fpr) to match. Of course, the
problem is that this only works for "full" bloom filters. Ranges with
fewer distinct values will have much lower probability, and ranges with
unexpectedly many distinct values will have much higher probability.

But I think we can ignore that, assume the index was created with good
parameters, so the bloom filters won't degrade and the target fpr is
probably a defensive value.

For minmax-multi, we probably should not ignore correlation entirely.
It does handle imperfect correlation much more gracefully than plain
minmax, but it still depends on reasonably ordered data.

A possible improvement would be to compute average "covering" of ranges,
i.e. given the length of a column domain

D = MAX(column) - MIN(column)

compute what fraction of that is covered by a range by summing lengths
of intervals in the range, and dividing it by D. And then averaging it
over all BRIN ranges.

This would allow us to estimate how many ranges are matched by a random
value from the column domain, I think. But this requires extending what
data analyze collects for indexes - I don't think there are any stats
specific to BRIN-specific collected at the moment.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-a-20200807.patchtext/plain; charset=us-asciiDownload
From 6ca56a8be03680ba398728ea862f6908b164fbf1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 13 Sep 2019 18:34:39 +0200
Subject: [PATCH 1/8] Pass all keys to BRIN consistent function at once

---
 src/backend/access/brin/brin.c           | 126 ++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 164 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 116 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 293 insertions(+), 121 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1f72562c60..6777d48faf 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,53 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +520,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,34 +530,19 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (!nkeys[attno - 1])
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
@@ -519,12 +554,43 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					 * the range as a whole, so break out of the loop as soon
 					 * as a false return value is obtained.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/* Check keys one by one */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..8968886ff5 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,103 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		matches;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = inclusion_consistent_key(bdesc, column, key, colloid);
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_BOOL(matches);
+}
+
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +376,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +437,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +458,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +477,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +512,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +522,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..1219a3a2ab 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,99 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
-	FmgrInfo   *finfo;
+	int			keyno;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = minmax_consistent_key(bdesc, column, key, colloid);
+
+		/* found non-matching key */
+		if (!matches)
+			break;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(matches);
+}
+
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +283,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 082a11f270..018a6490fb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8086,7 +8086,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8102,7 +8102,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.25.4

0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20200807.patchtext/plain; charset=us-asciiDownload
From e70a896cd8d38c6d90c22cd9ebb9a3fb5c229660 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:05:39 +0200
Subject: [PATCH 2/8] Move IS NOT NULL checks to bringetbitmap

---
 src/backend/access/brin/brin.c           | 116 ++++++++++++++++++++---
 src/backend/access/brin/brin_inclusion.c |  62 +-----------
 src/backend/access/brin/brin_minmax.c    |  62 +-----------
 3 files changed, 109 insertions(+), 131 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 6777d48faf..caf7b62688 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,8 +389,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +417,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -440,14 +445,12 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 							  keyattno - 1)->attcollation));
 
 		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -455,9 +458,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -544,6 +561,83 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys, and
+					 * if we're violating them. In that case we can terminate
+					 * early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					{
+						ScanKey	key = nullkeys[attno - 1][keyno];
+
+						Assert(key->sk_attno == bval->bv_attno);
+
+						/* interrupt the loop as soon as we find a mismatch */
+						if (!addrange)
+							break;
+
+						/* handle IS NULL/IS NOT NULL tests */
+						if (key->sk_flags & SK_ISNULL)
+						{
+							/* IS NULL scan key, but range has no NULLs */
+							if (key->sk_flags & SK_SEARCHNULL)
+							{
+								if (!bval->bv_allnulls && !bval->bv_hasnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * For IS NOT NULL, we can only skip ranges that are
+							 * known to have only nulls.
+							 */
+							if (key->sk_flags & SK_SEARCHNOTNULL)
+							{
+								if (bval->bv_allnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * Neither IS NULL nor IS NOT NULL was used; assume all
+							 * indexable operators are strict and thus return false
+							 * with NULL value in the scan key.
+							 */
+							addrange = false;
+						}
+					}
+
+					/*
+					 * If any of the IS [NOT] NULL keys failed, the page range as
+					 * a whole can't pass. So terminate the loop.
+					 */
+					if (!addrange)
+						break;
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed. If
+					 * there are no regular scan keys, we're done - the page range
+					 * matches. If there are regular keys, but the page range is
+					 * marked as 'all nulls' it can't possibly pass (we're assuming
+					 * the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 8968886ff5..22edc6b46f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -265,63 +265,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
 	bool		matches;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -333,9 +276,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = inclusion_consistent_key(bdesc, column, key, colloid);
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 1219a3a2ab..7a7bd21cec 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -153,63 +153,6 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	matches = true;
 
@@ -217,9 +160,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = minmax_consistent_key(bdesc, column, key, colloid);
 
-- 
2.25.4

0003-Move-processing-of-NULLs-from-BRIN-support--20200807.patchtext/plain; charset=us-asciiDownload
From 4381cd1ff3c3a822d969d4dc4e400b4d208606aa Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:56:00 +0200
Subject: [PATCH 3/8] Move processing of NULLs from BRIN support functions

---
 src/backend/access/brin/brin.c           | 260 ++++++++++++++---------
 src/backend/access/brin/brin_inclusion.c |  44 +---
 src/backend/access/brin/brin_minmax.c    |  41 +---
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 169 insertions(+), 179 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index caf7b62688..a9c44c0b82 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -359,6 +337,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -562,69 +541,31 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
-					 * First check if there are any IS [NOT] NULL scan keys, and
-					 * if we're violating them. In that case we can terminate
-					 * early, without invoking the support function.
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
 					 *
 					 * As there may be more keys, we can only detemine mismatch
 					 * within this loop.
 					 */
-					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
 					{
-						ScanKey	key = nullkeys[attno - 1][keyno];
-
-						Assert(key->sk_attno == bval->bv_attno);
-
-						/* interrupt the loop as soon as we find a mismatch */
-						if (!addrange)
-							break;
-
-						/* handle IS NULL/IS NOT NULL tests */
-						if (key->sk_flags & SK_ISNULL)
-						{
-							/* IS NULL scan key, but range has no NULLs */
-							if (key->sk_flags & SK_SEARCHNULL)
-							{
-								if (!bval->bv_allnulls && !bval->bv_hasnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * For IS NOT NULL, we can only skip ranges that are
-							 * known to have only nulls.
-							 */
-							if (key->sk_flags & SK_SEARCHNOTNULL)
-							{
-								if (bval->bv_allnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * Neither IS NULL nor IS NOT NULL was used; assume all
-							 * indexable operators are strict and thus return false
-							 * with NULL value in the scan key.
-							 */
-							addrange = false;
-						}
-					}
-
-					/*
-					 * If any of the IS [NOT] NULL keys failed, the page range as
-					 * a whole can't pass. So terminate the loop.
-					 */
-					if (!addrange)
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
 						break;
+					}
 
 					/*
-					 * So either there are no IS [NOT] NULL keys, or all passed. If
-					 * there are no regular scan keys, we're done - the page range
-					 * matches. If there are regular keys, but the page range is
-					 * marked as 'all nulls' it can't possibly pass (we're assuming
-					 * the operators are strict).
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
 					 */
 
 					/* No regular scan keys - page range as a whole passes. */
@@ -772,7 +713,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -801,25 +741,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1598,6 +1521,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1651,3 +1607,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 22edc6b46f..59503b6f68 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -510,37 +500,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 7a7bd21cec..8882eec12c 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -245,34 +235,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

0004-BRIN-bloom-indexes-20200807.patchtext/plain; charset=us-asciiDownload
From 104611d3dd82beafc9b15f015d9e7f84b761f1ce Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:57:59 +0200
Subject: [PATCH 4/8] BRIN bloom indexes

---
 doc/src/sgml/brin.sgml                   | 215 +++++
 doc/src/sgml/ref/create_index.sgml       |  31 +
 src/backend/access/brin/Makefile         |   1 +
 src/backend/access/brin/brin_bloom.c     | 980 +++++++++++++++++++++++
 src/include/access/brin.h                |   2 +
 src/include/access/brin_internal.h       |   4 +
 src/include/catalog/pg_amop.dat          | 165 ++++
 src/include/catalog/pg_amproc.dat        | 430 ++++++++++
 src/include/catalog/pg_opclass.dat       |  69 ++
 src/include/catalog/pg_opfamily.dat      |  36 +
 src/include/catalog/pg_proc.dat          |  20 +
 src/test/regress/expected/brin_bloom.out | 456 +++++++++++
 src/test/regress/expected/opr_sanity.out |   3 +-
 src/test/regress/expected/psql.out       |   3 +-
 src/test/regress/parallel_schedule       |   5 +
 src/test/regress/serial_schedule         |   1 +
 src/test/regress/sql/brin_bloom.sql      | 404 ++++++++++
 17 files changed, 2823 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 55b6272db6..e10fc64d6b 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -132,6 +132,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry><literal>int8_bloom_ops</literal></entry>
+     <entry><type>bigint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int8_minmax_ops</literal></entry>
      <entry><type>bigint</type></entry>
@@ -183,6 +190,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>|&amp;&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>bytea_bloom_ops</literal></entry>
+     <entry><type>bytea</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>bytea_minmax_ops</literal></entry>
      <entry><type>bytea</type></entry>
@@ -194,6 +208,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>bpchar_bloom_ops</literal></entry>
+     <entry><type>character</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>bpchar_minmax_ops</literal></entry>
      <entry><type>character</type></entry>
@@ -205,6 +226,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>char_bloom_ops</literal></entry>
+     <entry><type>"char"</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>char_minmax_ops</literal></entry>
      <entry><type>"char"</type></entry>
@@ -216,6 +244,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>date_bloom_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>date_minmax_ops</literal></entry>
      <entry><type>date</type></entry>
@@ -227,6 +262,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float8_bloom_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float8_minmax_ops</literal></entry>
      <entry><type>double precision</type></entry>
@@ -238,6 +280,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>inet_bloom_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>inet_minmax_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -261,6 +310,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&lt;&lt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>int4_bloom_ops</literal></entry>
+     <entry><type>integer</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int4_minmax_ops</literal></entry>
      <entry><type>integer</type></entry>
@@ -272,6 +328,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>interval_bloom_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>interval_minmax_ops</literal></entry>
      <entry><type>interval</type></entry>
@@ -283,6 +346,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr_bloom_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr_minmax_ops</literal></entry>
      <entry><type>macaddr</type></entry>
@@ -294,6 +364,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr8_minmax_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
@@ -305,6 +382,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>name_bloom_ops</literal></entry>
+     <entry><type>name</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>name_minmax_ops</literal></entry>
      <entry><type>name</type></entry>
@@ -316,6 +400,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>numeric_bloom_ops</literal></entry>
+     <entry><type>numeric</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>numeric_minmax_ops</literal></entry>
      <entry><type>numeric</type></entry>
@@ -327,6 +418,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><type>pg_lsn</type></entry>
@@ -338,6 +436,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>oid_bloom_ops</literal></entry>
+     <entry><type>oid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>oid_minmax_ops</literal></entry>
      <entry><type>oid</type></entry>
@@ -369,6 +474,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;=</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float4_bloom_ops</literal></entry>
+     <entry><type>real</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float4_minmax_ops</literal></entry>
      <entry><type>real</type></entry>
@@ -380,6 +492,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>int2_bloom_ops</literal></entry>
+     <entry><type>smallint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>int2_minmax_ops</literal></entry>
      <entry><type>smallint</type></entry>
@@ -391,6 +510,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>text_bloom_ops</literal></entry>
+     <entry><type>text</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>text_minmax_ops</literal></entry>
      <entry><type>text</type></entry>
@@ -413,6 +539,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamp_bloom_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamp_minmax_ops</literal></entry>
      <entry><type>timestamp without time zone</type></entry>
@@ -424,6 +557,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamptz_minmax_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
@@ -435,6 +575,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>time_bloom_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>time_minmax_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
@@ -446,6 +593,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timetz_bloom_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timetz_minmax_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
@@ -457,6 +611,13 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>uuid_bloom_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>uuid_minmax_ops</literal></entry>
      <entry><type>uuid</type></entry>
@@ -841,6 +1002,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 33aa64e81d..9c90d451a7 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -555,6 +555,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..b119e4e264
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,980 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i,
+			nvalues;
+	Size	len;
+	uint32 *values;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* TODO optimization: sort only the unsorted part, then merge */
+	pg_qsort(values, filter->nvalues, sizeof(uint32), cmp_uint32);
+
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (values[i] != values[nvalues - 1])
+			values[nvalues++] = values[i];
+	}
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..af6891e348 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2002,6 +2065,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2096,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2118,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2140,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2182,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2204,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2355,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2410,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2432,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2488,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2510,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2576,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 37b580883f..c24a80545a 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -770,6 +770,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -783,6 +801,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -796,6 +832,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -897,6 +951,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -910,6 +1016,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -922,6 +1046,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -984,6 +1125,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -998,6 +1178,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1012,6 +1212,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1025,6 +1245,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1059,6 +1297,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1072,6 +1330,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1179,6 +1455,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1193,6 +1525,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1207,6 +1559,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1247,6 +1619,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1260,6 +1652,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1295,6 +1705,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..16d6111ab6 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,127 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +386,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..7f733aeab1 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,86 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 018a6490fb..5336aa7b9c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8109,6 +8109,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 1b3c146e4c..dca8e9eb34 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2034,6 +2034,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2097,7 +2098,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 555d464f91..c1d19b1e0c 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4929,8 +4929,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..b49239b1d0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..abce8e180a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -107,6 +107,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.25.4

0005-add-special-pg_brin_bloom_summary-data-type-20200807.patchtext/plain; charset=us-asciiDownload
From b8ac6d8c00665ba2d2ae13777642cd35b2de4fe1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 6 Aug 2020 17:56:22 +0200
Subject: [PATCH 5/8] add special pg_brin_bloom_summary data type

---
 src/backend/access/brin/brin_bloom.c      | 91 ++++++++++++++++++++++-
 src/include/catalog/pg_proc.dat           | 14 ++++
 src/include/catalog/pg_type.dat           |  7 ++
 src/test/regress/expected/type_sanity.out |  7 +-
 4 files changed, 115 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index b119e4e264..e7ca100821 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -647,7 +647,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
-	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+	result->oi_typcache[0] = lookup_type_cache(BRINBLOOMSUMMARYOID, 0);
 
 	PG_RETURN_POINTER(result);
 }
@@ -978,3 +978,92 @@ brin_bloom_options(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5336aa7b9c..da4c9c6202 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10975,3 +10975,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index b2cec07416..a41c2e5418 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -631,3 +631,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034', oid_symbol => 'BRINBLOOMSUMMARYOID',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
-- 
2.25.4

0006-BRIN-multi-range-minmax-indexes-20200807.patchtext/plain; charset=us-asciiDownload
From 1b81e264cd2e6e27f3f2149468da774cf9faa3cc Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 6 Aug 2020 17:00:09 +0200
Subject: [PATCH 6/8] BRIN multi-range minmax indexes

---
 doc/src/sgml/brin.sgml                      |  212 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2178 +++++++++++++++++++
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/transam.h                |    8 +-
 src/include/catalog/pg_amop.dat             |  545 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   65 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 ++++
 17 files changed, 4498 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index e10fc64d6b..e1be48996d 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -262,6 +262,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>date_minmax_multi_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>float8_bloom_ops</literal></entry>
      <entry><type>double precision</type></entry>
@@ -280,6 +291,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>inet_bloom_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -298,6 +320,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>network_inclusion_ops</literal></entry>
      <entry><type>inet</type></entry>
@@ -346,6 +379,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr_bloom_ops</literal></entry>
      <entry><type>macaddr</type></entry>
@@ -364,6 +408,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>macaddr8_bloom_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
@@ -382,6 +437,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>name_bloom_ops</literal></entry>
      <entry><type>name</type></entry>
@@ -436,6 +502,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>oid_bloom_ops</literal></entry>
      <entry><type>oid</type></entry>
@@ -557,6 +634,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timestamptz_bloom_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
@@ -575,6 +663,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>time_bloom_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
@@ -593,6 +692,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>time_minmax_multi_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>timetz_bloom_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
@@ -611,6 +721,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
     <row>
      <entry><literal>uuid_bloom_ops</literal></entry>
      <entry><type>uuid</type></entry>
@@ -629,6 +750,17 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
    </tbody>
   </tgroup>
  </table>
@@ -753,13 +885,13 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions using
+  them are shipped for in-core data types as appropriate.  Additional operator
+  classes can be defined by the user for other data types using equivalent
+  definitions, without having to write any source code; appropriate catalog
+  entries being declared is enough.  Note that assumptions about the semantics
+  of operator strategies are embedded in the support functions' source code.
  </para>
 
  <para>
@@ -830,6 +962,72 @@ typedef struct BrinOpcInfo
   </tgroup>
  </table>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
   To write an operator class for a complex data type which has values
   included within another type, it's possible to use the inclusion support
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 9c90d451a7..f6b8fbd1d6 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -586,6 +586,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 64.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..9b9eedbb63
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2178 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+static Ranges *range_deserialize(SerializedRanges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (attr->attlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (attr->attlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(attr->attlen > 0);
+		len += nvalues * attr->attlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized,
+				AttrNumber attno, Form_pg_attribute attr)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTLessStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count_values(cranges, ncranges) <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		FmgrInfo   *cmpFn;
+		Datum	   *values;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												   BTLessStrategyNumber);
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		column->bv_allnulls = false;
+		modified = true;
+	}
+	else
+	{
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized, attno, attr);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+	if (modified)
+	{
+		SerializedRanges *s = range_serialize(ranges, attno, attr);
+		column->bv_values[0] = PointerGetDatum(s);
+
+		/*
+		 * XXX pfree must happen after range_serialize, because the Ranges value
+		 * may reference the original serialized value.
+		 */
+		if (serialized)
+			pfree(serialized);
+	}
+
+	pfree(ranges);
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+	Form_pg_attribute attr;
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized, attno, attr);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a, attno, attr);
+	ranges_b = range_deserialize(serialized_b, attno, attr);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attno, attr));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index a91a0c7487..18a8fd77d9 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -130,14 +130,14 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  *		when the .dat files in src/include/catalog/ do not specify an OID
  *		for a catalog entry that requires one.
  *
- *		OIDS 12000-16383 are reserved for assignment during initdb
- *		using the OID generator.  (We start the generator at 12000.)
+ *		OIDS 13000-16383 are reserved for assignment during initdb
+ *		using the OID generator.  (We start the generator at 13000.)
  *
  *		OIDs beginning at 16384 are assigned from the OID generator
  *		during normal multiuser operation.  (We force the generator up to
  *		16384 as soon as we are in normal operation.)
  *
- * The choices of 8000, 10000 and 12000 are completely arbitrary, and can be
+ * The choices of 8000, 10000 and 13000 are completely arbitrary, and can be
  * moved if we run low on OIDs in any category.  Changing the macros below,
  * and updating relevant documentation (see bki.sgml and RELEASE_CHANGES),
  * should be sufficient to do this.  Moving the 16384 boundary between
@@ -151,7 +151,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index af6891e348..31ec56766a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1900,6 +1900,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1977,6 +2123,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -1999,6 +2162,23 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2065,6 +2245,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2096,6 +2342,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2118,6 +2381,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2140,6 +2420,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2204,6 +2501,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2355,6 +2669,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2410,6 +2870,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2432,6 +2909,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2488,6 +2982,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2510,6 +3021,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2576,6 +3104,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index c24a80545a..317c9475f8 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -951,6 +951,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1046,6 +1192,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1075,6 +1238,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1125,6 +1305,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1178,6 +1432,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1212,6 +1486,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1245,6 +1539,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1330,6 +1644,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1455,6 +1788,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1525,6 +2022,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1559,6 +2076,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1619,6 +2156,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1650,7 +2207,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1705,6 +2283,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 16d6111ab6..92b3a577ea 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,38 +307,59 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -345,36 +375,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -386,6 +434,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -395,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -404,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 7f733aeab1..8bbf753f65 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,24 +220,36 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -240,10 +260,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -252,12 +276,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index da4c9c6202..59774edfa0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8092,6 +8092,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index c1d19b1e0c..05585225bc 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4926,12 +4926,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b49239b1d0..a933db5456 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index abce8e180a..3f05a7061f 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.25.4

0007-add-special-pg_brin_minmax_multi_summary-da-20200807.patchtext/plain; charset=us-asciiDownload
From 6a67fb31791ac4418f669e18ed5c42142e80bd6b Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 6 Aug 2020 18:20:28 +0200
Subject: [PATCH 7/8] add special pg_brin_minmax_multi_summary data type

---
 src/backend/access/brin/brin_minmax_multi.c | 260 +++++++++++++++++---
 src/include/catalog/pg_proc.dat             |  15 ++
 src/include/catalog/pg_type.dat             |   7 +
 3 files changed, 247 insertions(+), 35 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 9b9eedbb63..f700700d46 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -59,6 +59,7 @@
 #include "access/htup_details.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_amop.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/date.h"
 #include "utils/datum.h"
@@ -165,6 +166,9 @@ typedef struct SerializedRanges
 	/* varlena header (do not touch directly!) */
 	int32	vl_len_;
 
+	/* type of values stored in the data array */
+	Oid		typid;
+
 	/* (2*nranges + nvalues) <= maxvalues */
 	int		nranges;	/* number of ranges in the array (stored) */
 	int		nvalues;	/* number of values in the data array (all) */
@@ -174,11 +178,9 @@ typedef struct SerializedRanges
 	char	data[FLEXIBLE_ARRAY_MEMBER];
 } SerializedRanges;
 
-static SerializedRanges *range_serialize(Ranges *range,
-				AttrNumber attno, Form_pg_attribute attr);
+static SerializedRanges *range_serialize(Ranges *range, Oid typid);
 
-static Ranges *range_deserialize(SerializedRanges *range,
-				AttrNumber attno, Form_pg_attribute attr);
+static Ranges *range_deserialize(SerializedRanges *range);
 
 /* Cache for support and strategy procesures. */
 
@@ -223,11 +225,13 @@ minmax_multi_init(int maxvalues)
  * in the in-memory value array.
  */
 static SerializedRanges *
-range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
+range_serialize(Ranges *range, Oid typid)
 {
 	Size	len;
 	int		nvalues;
 	SerializedRanges *serialized;
+	int		typlen;
+	bool	typbyval;
 
 	int		i;
 	char   *ptr;
@@ -242,6 +246,9 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 
 	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
 
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
 	/* header is always needed */
 	len = offsetof(SerializedRanges,data);
 
@@ -251,7 +258,7 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 	 * (attlen * nvalues) and we're done. For variable-length by-reference
 	 * types we need to actually walk all the values and sum the lengths.
 	 */
-	if (attr->attlen == -1)	/* varlena */
+	if (typlen == -1)	/* varlena */
 	{
 		int i;
 		for (i = 0; i < nvalues; i++)
@@ -259,7 +266,7 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 			len += VARSIZE_ANY(range->values[i]);
 		}
 	}
-	else if (attr->attlen == -2)	/* cstring */
+	else if (typlen == -2)	/* cstring */
 	{
 		int i;
 		for (i = 0; i < nvalues; i++)
@@ -270,8 +277,8 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 	}
 	else /* fixed-length types (even by-reference) */
 	{
-		Assert(attr->attlen > 0);
-		len += nvalues * attr->attlen;
+		Assert(typlen > 0);
+		len += nvalues * typlen;
 	}
 
 	/*
@@ -281,6 +288,7 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 	serialized = (SerializedRanges *) palloc0(len);
 	SET_VARSIZE(serialized, len);
 
+	serialized->typid = typid;
 	serialized->nranges = range->nranges;
 	serialized->nvalues = range->nvalues;
 	serialized->maxvalues = range->maxvalues;
@@ -293,23 +301,23 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 
 	for (i = 0; i < nvalues; i++)
 	{
-		if (attr->attbyval)	/* simple by-value data types */
+		if (typbyval)	/* simple by-value data types */
 		{
-			memcpy(ptr, &range->values[i], attr->attlen);
-			ptr += attr->attlen;
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
 		}
-		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		else if (typlen > 0)	/* fixed-length by-ref types */
 		{
-			memcpy(ptr, DatumGetPointer(range->values[i]), attr->attlen);
-			ptr += attr->attlen;
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
 		}
-		else if (attr->attlen == -1)	/* varlena */
+		else if (typlen == -1)	/* varlena */
 		{
 			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
 			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
 			ptr += tmp;
 		}
-		else if (attr->attlen == -2)	/* cstring */
+		else if (typlen == -2)	/* cstring */
 		{
 			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
 			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
@@ -334,12 +342,13 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
  * in the in-memory value array.
  */
 static Ranges *
-range_deserialize(SerializedRanges *serialized,
-				AttrNumber attno, Form_pg_attribute attr)
+range_deserialize(SerializedRanges *serialized)
 {
 	int		i,
 			nvalues;
 	char   *ptr;
+	bool	typbyval;
+	int		typlen;
 
 	Ranges *range;
 
@@ -358,6 +367,9 @@ range_deserialize(SerializedRanges *serialized,
 	range->nvalues = serialized->nvalues;
 	range->maxvalues = serialized->maxvalues;
 
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
 	/*
 	 * And now deconstruct the values into Datum array. We don't need
 	 * to copy the values and will instead just point the values to the
@@ -367,23 +379,23 @@ range_deserialize(SerializedRanges *serialized,
 
 	for (i = 0; i < nvalues; i++)
 	{
-		if (attr->attbyval)	/* simple by-value data types */
+		if (typbyval)	/* simple by-value data types */
 		{
-			memcpy(&range->values[i], ptr, attr->attlen);
-			ptr += attr->attlen;
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
 		}
-		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		else if (typlen > 0)	/* fixed-length by-ref types */
 		{
 			/* no copy, just set the value to the pointer */
 			range->values[i] = PointerGetDatum(ptr);
-			ptr += attr->attlen;
+			ptr += typlen;
 		}
-		else if (attr->attlen == -1)	/* varlena */
+		else if (typlen == -1)	/* varlena */
 		{
 			range->values[i] = PointerGetDatum(ptr);
 			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
 		}
-		else if (attr->attlen == -2)	/* cstring */
+		else if (typlen == -2)	/* cstring */
 		{
 			range->values[i] = PointerGetDatum(ptr);
 			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
@@ -1293,7 +1305,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
-	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+	result->oi_typcache[0] = lookup_type_cache(BRINMINMAXMULTISUMMARYOID, 0);
 
 	PG_RETURN_POINTER(result);
 }
@@ -1738,7 +1750,7 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	else
 	{
 		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-		ranges = range_deserialize(serialized, attno, attr);
+		ranges = range_deserialize(serialized);
 	}
 
 	/*
@@ -1749,7 +1761,7 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 
 	if (modified)
 	{
-		SerializedRanges *s = range_serialize(ranges, attno, attr);
+		SerializedRanges *s = range_serialize(ranges, attr->atttypid);
 		column->bv_values[0] = PointerGetDatum(s);
 
 		/*
@@ -1788,13 +1800,11 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
 	int			keyno;
 	int			rangeno;
 	int			i;
-	Form_pg_attribute attr;
 
 	attno = column->bv_attno;
-	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
 	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-	ranges = range_deserialize(serialized, attno, attr);
+	ranges = range_deserialize(serialized);
 
 	/* inspect the ranges, and for each one evaluate the scan keys */
 	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
@@ -1981,8 +1991,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
 	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
 
-	ranges_a = range_deserialize(serialized_a, attno, attr);
-	ranges_b = range_deserialize(serialized_b, attno, attr);
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
 
 	/* make sure neither of the ranges is NULL */
 	Assert(ranges_a && ranges_b);
@@ -2052,7 +2062,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 
 	/* cleanup and update the serialized value */
 	pfree(serialized_a);
-	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attno, attr));
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attr->atttypid));
 
 	PG_RETURN_VOID();
 }
@@ -2176,3 +2186,183 @@ brin_minmax_multi_options(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
+
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 59774edfa0..f88e85532f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11054,3 +11054,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index a41c2e5418..c189b35a3d 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -638,3 +638,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039', oid_symbol => 'BRINMINMAXMULTISUMMARYOID',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
-- 
2.25.4

0008-tweak-costing-for-bloom-minmax-multi-indexe-20200807.patchtext/plain; charset=us-asciiDownload
From f3b590c9a3484d3c7c5f8e36c8e92218371a6f2a Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Fri, 7 Aug 2020 15:53:55 +0200
Subject: [PATCH 8/8] tweak costing for bloom/minmax-multi indexes

---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index e7ca100821..f64381b657 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -643,6 +643,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index f700700d46..5f637e0629 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1301,6 +1301,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 53d974125f..956d6f0a84 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7348,7 +7349,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7370,6 +7372,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7403,6 +7406,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7473,6 +7487,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

#80Michael Paquier
michael@paquier.xyz
In reply to: Tomas Vondra (#79)
Re: WIP: BRIN multi-range indexes

On Fri, Aug 07, 2020 at 06:27:01PM +0200, Tomas Vondra wrote:

Attached is an updated version of the patch series, implementing this.
Adding the extra data types was fairly simple, because both bloom and
minmax-multi indexes already used "struct as varlena" approach, so all
that needed was a bunch of in/out functions and catalog records.

I've left the changes in separate patches for clarity, ultimately it'll
get merged into the other parts.

This fails to apply per the CF bot, so please provide a rebase.
--
Michael

#81Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Michael Paquier (#80)
8 attachment(s)
Re: WIP: BRIN multi-range indexes

On Sat, Sep 05, 2020 at 10:46:48AM +0900, Michael Paquier wrote:

On Fri, Aug 07, 2020 at 06:27:01PM +0200, Tomas Vondra wrote:

Attached is an updated version of the patch series, implementing this.
Adding the extra data types was fairly simple, because both bloom and
minmax-multi indexes already used "struct as varlena" approach, so all
that needed was a bunch of in/out functions and catalog records.

I've left the changes in separate patches for clarity, ultimately it'll
get merged into the other parts.

This fails to apply per the CF bot, so please provide a rebase.

OK, here is a rebased version. Most of the breakage was due to changes
to the BRIN sgml docs.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-a-20200906.patchtext/plain; charset=us-asciiDownload
From 7505b3e71950818388d6f0dbb7a14791156b56c4 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 13 Sep 2019 18:34:39 +0200
Subject: [PATCH 1/8] Pass all keys to BRIN consistent function at once

---
 src/backend/access/brin/brin.c           | 126 ++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 164 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 116 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 293 insertions(+), 121 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1f72562c60..6777d48faf 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,53 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +520,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,34 +530,19 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (!nkeys[attno - 1])
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
@@ -519,12 +554,43 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					 * the range as a whole, so break out of the loop as soon
 					 * as a false return value is obtained.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/* Check keys one by one */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..8968886ff5 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,103 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		matches;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = inclusion_consistent_key(bdesc, column, key, colloid);
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_BOOL(matches);
+}
+
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +376,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +437,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +458,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +477,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +512,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +522,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..1219a3a2ab 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,99 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
-	FmgrInfo   *finfo;
+	int			keyno;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = minmax_consistent_key(bdesc, column, key, colloid);
+
+		/* found non-matching key */
+		if (!matches)
+			break;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(matches);
+}
+
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +283,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 687509ba92..925b262b60 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8104,7 +8104,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8120,7 +8120,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.25.4

0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20200906.patchtext/plain; charset=us-asciiDownload
From 522651c2996031b429713529092407fd7688c049 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:05:39 +0200
Subject: [PATCH 2/8] Move IS NOT NULL checks to bringetbitmap

---
 src/backend/access/brin/brin.c           | 116 ++++++++++++++++++++---
 src/backend/access/brin/brin_inclusion.c |  62 +-----------
 src/backend/access/brin/brin_minmax.c    |  62 +-----------
 3 files changed, 109 insertions(+), 131 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 6777d48faf..caf7b62688 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,8 +389,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +417,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -440,14 +445,12 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 							  keyattno - 1)->attcollation));
 
 		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -455,9 +458,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -544,6 +561,83 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys, and
+					 * if we're violating them. In that case we can terminate
+					 * early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					{
+						ScanKey	key = nullkeys[attno - 1][keyno];
+
+						Assert(key->sk_attno == bval->bv_attno);
+
+						/* interrupt the loop as soon as we find a mismatch */
+						if (!addrange)
+							break;
+
+						/* handle IS NULL/IS NOT NULL tests */
+						if (key->sk_flags & SK_ISNULL)
+						{
+							/* IS NULL scan key, but range has no NULLs */
+							if (key->sk_flags & SK_SEARCHNULL)
+							{
+								if (!bval->bv_allnulls && !bval->bv_hasnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * For IS NOT NULL, we can only skip ranges that are
+							 * known to have only nulls.
+							 */
+							if (key->sk_flags & SK_SEARCHNOTNULL)
+							{
+								if (bval->bv_allnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * Neither IS NULL nor IS NOT NULL was used; assume all
+							 * indexable operators are strict and thus return false
+							 * with NULL value in the scan key.
+							 */
+							addrange = false;
+						}
+					}
+
+					/*
+					 * If any of the IS [NOT] NULL keys failed, the page range as
+					 * a whole can't pass. So terminate the loop.
+					 */
+					if (!addrange)
+						break;
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed. If
+					 * there are no regular scan keys, we're done - the page range
+					 * matches. If there are regular keys, but the page range is
+					 * marked as 'all nulls' it can't possibly pass (we're assuming
+					 * the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 8968886ff5..22edc6b46f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -265,63 +265,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
 	bool		matches;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -333,9 +276,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = inclusion_consistent_key(bdesc, column, key, colloid);
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 1219a3a2ab..7a7bd21cec 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -153,63 +153,6 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	matches = true;
 
@@ -217,9 +160,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = minmax_consistent_key(bdesc, column, key, colloid);
 
-- 
2.25.4

0003-Move-processing-of-NULLs-from-BRIN-support--20200906.patchtext/plain; charset=us-asciiDownload
From 2ca96d036bb3dd27a2f6a4ad4649bcdf8630736a Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:56:00 +0200
Subject: [PATCH 3/8] Move processing of NULLs from BRIN support functions

---
 src/backend/access/brin/brin.c           | 260 ++++++++++++++---------
 src/backend/access/brin/brin_inclusion.c |  44 +---
 src/backend/access/brin/brin_minmax.c    |  41 +---
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 169 insertions(+), 179 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index caf7b62688..a9c44c0b82 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -359,6 +337,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -562,69 +541,31 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
-					 * First check if there are any IS [NOT] NULL scan keys, and
-					 * if we're violating them. In that case we can terminate
-					 * early, without invoking the support function.
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
 					 *
 					 * As there may be more keys, we can only detemine mismatch
 					 * within this loop.
 					 */
-					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
 					{
-						ScanKey	key = nullkeys[attno - 1][keyno];
-
-						Assert(key->sk_attno == bval->bv_attno);
-
-						/* interrupt the loop as soon as we find a mismatch */
-						if (!addrange)
-							break;
-
-						/* handle IS NULL/IS NOT NULL tests */
-						if (key->sk_flags & SK_ISNULL)
-						{
-							/* IS NULL scan key, but range has no NULLs */
-							if (key->sk_flags & SK_SEARCHNULL)
-							{
-								if (!bval->bv_allnulls && !bval->bv_hasnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * For IS NOT NULL, we can only skip ranges that are
-							 * known to have only nulls.
-							 */
-							if (key->sk_flags & SK_SEARCHNOTNULL)
-							{
-								if (bval->bv_allnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * Neither IS NULL nor IS NOT NULL was used; assume all
-							 * indexable operators are strict and thus return false
-							 * with NULL value in the scan key.
-							 */
-							addrange = false;
-						}
-					}
-
-					/*
-					 * If any of the IS [NOT] NULL keys failed, the page range as
-					 * a whole can't pass. So terminate the loop.
-					 */
-					if (!addrange)
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
 						break;
+					}
 
 					/*
-					 * So either there are no IS [NOT] NULL keys, or all passed. If
-					 * there are no regular scan keys, we're done - the page range
-					 * matches. If there are regular keys, but the page range is
-					 * marked as 'all nulls' it can't possibly pass (we're assuming
-					 * the operators are strict).
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
 					 */
 
 					/* No regular scan keys - page range as a whole passes. */
@@ -772,7 +713,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -801,25 +741,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1598,6 +1521,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1651,3 +1607,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 22edc6b46f..59503b6f68 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -510,37 +500,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 7a7bd21cec..8882eec12c 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -245,34 +235,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

0004-BRIN-bloom-indexes-20200906.patchtext/plain; charset=us-asciiDownload
From fb109e862884f4184734ea2483941e754be25bb6 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sat, 5 Sep 2020 23:54:39 +0200
Subject: [PATCH 4/8] BRIN bloom indexes

---
 doc/src/sgml/brin.sgml                   | 173 ++++
 doc/src/sgml/ref/create_index.sgml       |  31 +
 src/backend/access/brin/Makefile         |   1 +
 src/backend/access/brin/brin_bloom.c     | 980 +++++++++++++++++++++++
 src/include/access/brin.h                |   2 +
 src/include/access/brin_internal.h       |   4 +
 src/include/catalog/pg_amop.dat          | 165 ++++
 src/include/catalog/pg_amproc.dat        | 430 ++++++++++
 src/include/catalog/pg_opclass.dat       |  69 ++
 src/include/catalog/pg_opfamily.dat      |  36 +
 src/include/catalog/pg_proc.dat          |  20 +
 src/test/regress/expected/brin_bloom.out | 456 +++++++++++
 src/test/regress/expected/opr_sanity.out |   3 +-
 src/test/regress/expected/psql.out       |   3 +-
 src/test/regress/parallel_schedule       |   5 +
 src/test/regress/serial_schedule         |   1 +
 src/test/regress/sql/brin_bloom.sql      | 404 ++++++++++
 17 files changed, 2781 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..9b1d94b772 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -353,6 +447,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +461,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +475,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +489,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +503,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +898,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 33aa64e81d..9c90d451a7 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -555,6 +555,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..b119e4e264
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,980 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i,
+			nvalues;
+	Size	len;
+	uint32 *values;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* TODO optimization: sort only the unsorted part, then merge */
+	pg_qsort(values, filter->nvalues, sizeof(uint32), cmp_uint32);
+
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (values[i] != values[nvalues - 1])
+			values[nvalues++] = values[i];
+	}
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..af6891e348 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2002,6 +2065,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2096,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2118,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2140,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2182,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2204,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2355,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2410,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2432,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2488,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2510,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2576,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 37b580883f..c24a80545a 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -770,6 +770,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -783,6 +801,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -796,6 +832,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -897,6 +951,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -910,6 +1016,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -922,6 +1046,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -984,6 +1125,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -998,6 +1178,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1012,6 +1212,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1025,6 +1245,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1059,6 +1297,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1072,6 +1330,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1179,6 +1455,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1193,6 +1525,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1207,6 +1559,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1247,6 +1619,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1260,6 +1652,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1295,6 +1705,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..16d6111ab6 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,127 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +386,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..7f733aeab1 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,86 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 925b262b60..c00e4ece94 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8127,6 +8127,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 1b3c146e4c..dca8e9eb34 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2034,6 +2034,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2097,7 +2098,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index daac0ff49d..72c7598c89 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4989,8 +4989,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..b49239b1d0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..abce8e180a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -107,6 +107,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.25.4

0005-add-special-pg_brin_bloom_summary-data-type-20200906.patchtext/plain; charset=us-asciiDownload
From 7510f03d03843da4738d0711dad2e3204d68d9ca Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 6 Aug 2020 17:56:22 +0200
Subject: [PATCH 5/8] add special pg_brin_bloom_summary data type

---
 src/backend/access/brin/brin_bloom.c      | 91 ++++++++++++++++++++++-
 src/include/catalog/pg_proc.dat           | 14 ++++
 src/include/catalog/pg_type.dat           |  7 ++
 src/test/regress/expected/type_sanity.out |  7 +-
 4 files changed, 115 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index b119e4e264..e7ca100821 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -647,7 +647,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
-	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+	result->oi_typcache[0] = lookup_type_cache(BRINBLOOMSUMMARYOID, 0);
 
 	PG_RETURN_POINTER(result);
 }
@@ -978,3 +978,92 @@ brin_bloom_options(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c00e4ece94..dc17d4ce38 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10993,3 +10993,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index b2cec07416..a41c2e5418 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -631,3 +631,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034', oid_symbol => 'BRINBLOOMSUMMARYOID',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
-- 
2.25.4

0006-BRIN-minmax-multi-indexes-20200906.patchtext/plain; charset=us-asciiDownload
From 5b29d840c3e62ff86581d752e4eecdc828675d91 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 6 Sep 2020 00:10:44 +0200
Subject: [PATCH 6/8] BRIN minmax-multi indexes

---
 doc/src/sgml/brin.sgml                      |  252 ++-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2178 +++++++++++++++++++
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/transam.h                |    8 +-
 src/include/catalog/pg_amop.dat             |  545 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   65 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 ++++
 17 files changed, 4538 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 9b1d94b772..46300dada5 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -447,6 +564,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -461,6 +587,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -475,6 +610,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -489,6 +633,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -503,6 +656,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -517,6 +679,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -649,13 +820,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -952,6 +1124,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 9c90d451a7..f6b8fbd1d6 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -586,6 +586,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 64.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..9b9eedbb63
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2178 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+static Ranges *range_deserialize(SerializedRanges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (attr->attlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (attr->attlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(attr->attlen > 0);
+		len += nvalues * attr->attlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized,
+				AttrNumber attno, Form_pg_attribute attr)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTLessStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count_values(cranges, ncranges) <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		FmgrInfo   *cmpFn;
+		Datum	   *values;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												   BTLessStrategyNumber);
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		column->bv_allnulls = false;
+		modified = true;
+	}
+	else
+	{
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized, attno, attr);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+	if (modified)
+	{
+		SerializedRanges *s = range_serialize(ranges, attno, attr);
+		column->bv_values[0] = PointerGetDatum(s);
+
+		/*
+		 * XXX pfree must happen after range_serialize, because the Ranges value
+		 * may reference the original serialized value.
+		 */
+		if (serialized)
+			pfree(serialized);
+	}
+
+	pfree(ranges);
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+	Form_pg_attribute attr;
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized, attno, attr);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a, attno, attr);
+	ranges_b = range_deserialize(serialized_b, attno, attr);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attno, attr));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 2f1f144db4..b0993f8ac5 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -165,14 +165,14 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  *		when the .dat files in src/include/catalog/ do not specify an OID
  *		for a catalog entry that requires one.
  *
- *		OIDS 12000-16383 are reserved for assignment during initdb
- *		using the OID generator.  (We start the generator at 12000.)
+ *		OIDS 13000-16383 are reserved for assignment during initdb
+ *		using the OID generator.  (We start the generator at 13000.)
  *
  *		OIDs beginning at 16384 are assigned from the OID generator
  *		during normal multiuser operation.  (We force the generator up to
  *		16384 as soon as we are in normal operation.)
  *
- * The choices of 8000, 10000 and 12000 are completely arbitrary, and can be
+ * The choices of 8000, 10000 and 13000 are completely arbitrary, and can be
  * moved if we run low on OIDs in any category.  Changing the macros below,
  * and updating relevant documentation (see bki.sgml and RELEASE_CHANGES),
  * should be sufficient to do this.  Moving the 16384 boundary between
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index af6891e348..31ec56766a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1900,6 +1900,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1977,6 +2123,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -1999,6 +2162,23 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2065,6 +2245,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2096,6 +2342,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2118,6 +2381,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2140,6 +2420,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2204,6 +2501,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2355,6 +2669,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2410,6 +2870,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2432,6 +2909,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2488,6 +2982,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2510,6 +3021,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2576,6 +3104,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index c24a80545a..317c9475f8 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -951,6 +951,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1046,6 +1192,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1075,6 +1238,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1125,6 +1305,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1178,6 +1432,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1212,6 +1486,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1245,6 +1539,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1330,6 +1644,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1455,6 +1788,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1525,6 +2022,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1559,6 +2076,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1619,6 +2156,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1650,7 +2207,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1705,6 +2283,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 16d6111ab6..92b3a577ea 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,38 +307,59 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -345,36 +375,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -386,6 +434,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -395,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -404,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 7f733aeab1..8bbf753f65 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,24 +220,36 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -240,10 +260,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -252,12 +276,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dc17d4ce38..1936e06537 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8110,6 +8110,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 72c7598c89..f855633634 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4986,12 +4986,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b49239b1d0..a933db5456 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index abce8e180a..3f05a7061f 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.25.4

0007-add-special-pg_brin_minmax_multi_summary-da-20200906.patchtext/plain; charset=us-asciiDownload
From cced1711bbb6528bcae461d5ea8d5529f7859962 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 6 Aug 2020 18:20:28 +0200
Subject: [PATCH 7/8] add special pg_brin_minmax_multi_summary data type

---
 src/backend/access/brin/brin_minmax_multi.c | 260 +++++++++++++++++---
 src/include/catalog/pg_proc.dat             |  15 ++
 src/include/catalog/pg_type.dat             |   7 +
 3 files changed, 247 insertions(+), 35 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 9b9eedbb63..f700700d46 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -59,6 +59,7 @@
 #include "access/htup_details.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_amop.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/date.h"
 #include "utils/datum.h"
@@ -165,6 +166,9 @@ typedef struct SerializedRanges
 	/* varlena header (do not touch directly!) */
 	int32	vl_len_;
 
+	/* type of values stored in the data array */
+	Oid		typid;
+
 	/* (2*nranges + nvalues) <= maxvalues */
 	int		nranges;	/* number of ranges in the array (stored) */
 	int		nvalues;	/* number of values in the data array (all) */
@@ -174,11 +178,9 @@ typedef struct SerializedRanges
 	char	data[FLEXIBLE_ARRAY_MEMBER];
 } SerializedRanges;
 
-static SerializedRanges *range_serialize(Ranges *range,
-				AttrNumber attno, Form_pg_attribute attr);
+static SerializedRanges *range_serialize(Ranges *range, Oid typid);
 
-static Ranges *range_deserialize(SerializedRanges *range,
-				AttrNumber attno, Form_pg_attribute attr);
+static Ranges *range_deserialize(SerializedRanges *range);
 
 /* Cache for support and strategy procesures. */
 
@@ -223,11 +225,13 @@ minmax_multi_init(int maxvalues)
  * in the in-memory value array.
  */
 static SerializedRanges *
-range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
+range_serialize(Ranges *range, Oid typid)
 {
 	Size	len;
 	int		nvalues;
 	SerializedRanges *serialized;
+	int		typlen;
+	bool	typbyval;
 
 	int		i;
 	char   *ptr;
@@ -242,6 +246,9 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 
 	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
 
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
 	/* header is always needed */
 	len = offsetof(SerializedRanges,data);
 
@@ -251,7 +258,7 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 	 * (attlen * nvalues) and we're done. For variable-length by-reference
 	 * types we need to actually walk all the values and sum the lengths.
 	 */
-	if (attr->attlen == -1)	/* varlena */
+	if (typlen == -1)	/* varlena */
 	{
 		int i;
 		for (i = 0; i < nvalues; i++)
@@ -259,7 +266,7 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 			len += VARSIZE_ANY(range->values[i]);
 		}
 	}
-	else if (attr->attlen == -2)	/* cstring */
+	else if (typlen == -2)	/* cstring */
 	{
 		int i;
 		for (i = 0; i < nvalues; i++)
@@ -270,8 +277,8 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 	}
 	else /* fixed-length types (even by-reference) */
 	{
-		Assert(attr->attlen > 0);
-		len += nvalues * attr->attlen;
+		Assert(typlen > 0);
+		len += nvalues * typlen;
 	}
 
 	/*
@@ -281,6 +288,7 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 	serialized = (SerializedRanges *) palloc0(len);
 	SET_VARSIZE(serialized, len);
 
+	serialized->typid = typid;
 	serialized->nranges = range->nranges;
 	serialized->nvalues = range->nvalues;
 	serialized->maxvalues = range->maxvalues;
@@ -293,23 +301,23 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 
 	for (i = 0; i < nvalues; i++)
 	{
-		if (attr->attbyval)	/* simple by-value data types */
+		if (typbyval)	/* simple by-value data types */
 		{
-			memcpy(ptr, &range->values[i], attr->attlen);
-			ptr += attr->attlen;
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
 		}
-		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		else if (typlen > 0)	/* fixed-length by-ref types */
 		{
-			memcpy(ptr, DatumGetPointer(range->values[i]), attr->attlen);
-			ptr += attr->attlen;
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
 		}
-		else if (attr->attlen == -1)	/* varlena */
+		else if (typlen == -1)	/* varlena */
 		{
 			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
 			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
 			ptr += tmp;
 		}
-		else if (attr->attlen == -2)	/* cstring */
+		else if (typlen == -2)	/* cstring */
 		{
 			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
 			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
@@ -334,12 +342,13 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
  * in the in-memory value array.
  */
 static Ranges *
-range_deserialize(SerializedRanges *serialized,
-				AttrNumber attno, Form_pg_attribute attr)
+range_deserialize(SerializedRanges *serialized)
 {
 	int		i,
 			nvalues;
 	char   *ptr;
+	bool	typbyval;
+	int		typlen;
 
 	Ranges *range;
 
@@ -358,6 +367,9 @@ range_deserialize(SerializedRanges *serialized,
 	range->nvalues = serialized->nvalues;
 	range->maxvalues = serialized->maxvalues;
 
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
 	/*
 	 * And now deconstruct the values into Datum array. We don't need
 	 * to copy the values and will instead just point the values to the
@@ -367,23 +379,23 @@ range_deserialize(SerializedRanges *serialized,
 
 	for (i = 0; i < nvalues; i++)
 	{
-		if (attr->attbyval)	/* simple by-value data types */
+		if (typbyval)	/* simple by-value data types */
 		{
-			memcpy(&range->values[i], ptr, attr->attlen);
-			ptr += attr->attlen;
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
 		}
-		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		else if (typlen > 0)	/* fixed-length by-ref types */
 		{
 			/* no copy, just set the value to the pointer */
 			range->values[i] = PointerGetDatum(ptr);
-			ptr += attr->attlen;
+			ptr += typlen;
 		}
-		else if (attr->attlen == -1)	/* varlena */
+		else if (typlen == -1)	/* varlena */
 		{
 			range->values[i] = PointerGetDatum(ptr);
 			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
 		}
-		else if (attr->attlen == -2)	/* cstring */
+		else if (typlen == -2)	/* cstring */
 		{
 			range->values[i] = PointerGetDatum(ptr);
 			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
@@ -1293,7 +1305,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
-	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+	result->oi_typcache[0] = lookup_type_cache(BRINMINMAXMULTISUMMARYOID, 0);
 
 	PG_RETURN_POINTER(result);
 }
@@ -1738,7 +1750,7 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	else
 	{
 		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-		ranges = range_deserialize(serialized, attno, attr);
+		ranges = range_deserialize(serialized);
 	}
 
 	/*
@@ -1749,7 +1761,7 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 
 	if (modified)
 	{
-		SerializedRanges *s = range_serialize(ranges, attno, attr);
+		SerializedRanges *s = range_serialize(ranges, attr->atttypid);
 		column->bv_values[0] = PointerGetDatum(s);
 
 		/*
@@ -1788,13 +1800,11 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
 	int			keyno;
 	int			rangeno;
 	int			i;
-	Form_pg_attribute attr;
 
 	attno = column->bv_attno;
-	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
 	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-	ranges = range_deserialize(serialized, attno, attr);
+	ranges = range_deserialize(serialized);
 
 	/* inspect the ranges, and for each one evaluate the scan keys */
 	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
@@ -1981,8 +1991,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
 	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
 
-	ranges_a = range_deserialize(serialized_a, attno, attr);
-	ranges_b = range_deserialize(serialized_b, attno, attr);
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
 
 	/* make sure neither of the ranges is NULL */
 	Assert(ranges_a && ranges_b);
@@ -2052,7 +2062,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 
 	/* cleanup and update the serialized value */
 	pfree(serialized_a);
-	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attno, attr));
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attr->atttypid));
 
 	PG_RETURN_VOID();
 }
@@ -2176,3 +2186,183 @@ brin_minmax_multi_options(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
+
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 1936e06537..77735c3abe 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11072,3 +11072,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index a41c2e5418..c189b35a3d 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -638,3 +638,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039', oid_symbol => 'BRINMINMAXMULTISUMMARYOID',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
-- 
2.25.4

0008-tweak-costing-for-bloom-minmax-multi-indexe-20200906.patchtext/plain; charset=us-asciiDownload
From d30c2bc9d289c8cac373d200ea4574d7a222c85f Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Fri, 7 Aug 2020 15:53:55 +0200
Subject: [PATCH 8/8] tweak costing for bloom/minmax-multi indexes

---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index e7ca100821..f64381b657 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -643,6 +643,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index f700700d46..5f637e0629 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1301,6 +1301,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 00c7afc66f..c00265a66d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7350,7 +7351,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7372,6 +7374,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7405,6 +7408,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7475,6 +7489,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

#82John Naylor
john.naylor@2ndquadrant.com
In reply to: Tomas Vondra (#81)
Re: WIP: BRIN multi-range indexes

On Sat, Sep 5, 2020 at 7:21 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

OK, here is a rebased version. Most of the breakage was due to changes
to the BRIN sgml docs.

Hi Tomas,

I plan on trying some different queries on different data
distributions to get a sense of when the planner chooses a
multi-minmax index, and whether the choice is good.

Just to start, I used the artificial example in [1]/messages/by-id/459eef3e-48c7-0f5a-8356-992442a78bb6@2ndquadrant.com, but scaled down a
bit to save time. Config is at the default except for:
shared_buffers = 1GB
random_page_cost = 1.1;
effective_cache_size = 4GB;

create table t (a bigint, b int) with (fillfactor=95);

insert into t select i + 1000*random(), i+1000*random()
from generate_series(1,10000000) s(i);

update t set a = 1, b = 1 where random() < 0.001;
update t set a = 10000000, b = 10000000 where random() < 0.001;

analyze t;

create index on t using brin (a);
CREATE INDEX
Time: 1631.452 ms (00:01.631)

explain analyze select * from t
where a between 1923300::int and 1923600::int;

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on t (cost=16.10..43180.43 rows=291 width=12)
(actual time=217.770..1131.366 rows=288 loops=1)
Recheck Cond: ((a >= 1923300) AND (a <= 1923600))
Rows Removed by Index Recheck: 9999712
Heap Blocks: lossy=56819
-> Bitmap Index Scan on t_a_idx (cost=0.00..16.03 rows=22595
width=0) (actual time=3.054..3.055 rows=568320 loops=1)
Index Cond: ((a >= 1923300) AND (a <= 1923600))
Planning Time: 0.328 ms
Execution Time: 1131.411 ms
(8 rows)

Now add the multi-minmax:

create index on t using brin (a int8_minmax_multi_ops);
CREATE INDEX
Time: 6521.026 ms (00:06.521)

The first interesting thing is, with both BRIN indexes available, the
planner still chooses the conventional BRIN index. Only when I disable
it, does it choose the multi-minmax index:

explain analyze select * from t
where a between 1923300::int and 1923600::int;

QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on t (cost=68.10..43160.86 rows=291 width=12)
(actual time=1.835..4.196 rows=288 loops=1)
Recheck Cond: ((a >= 1923300) AND (a <= 1923600))
Rows Removed by Index Recheck: 22240
Heap Blocks: lossy=128
-> Bitmap Index Scan on t_a_idx1 (cost=0.00..68.03 rows=22523
width=0) (actual time=0.691..0.691 rows=1280 loops=1)
Index Cond: ((a >= 1923300) AND (a <= 1923600))
Planning Time: 0.250 ms
Execution Time: 4.244 ms
(8 rows)

I wonder if this is a clue that something in the costing unfairly
penalizes a multi-minmax index. Maybe not enough to matter in
practice, since I wouldn't expect a user to put different kinds of
index on the same column.

The second thing is, with parallel seq scan, the query is faster than
a BRIN bitmap scan, with this pathological data distribution, but the
planner won't choose it unless forced to:

set enable_bitmapscan = 'off';
explain analyze select * from t
where a between 1923300::int and 1923600::int;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..120348.10 rows=291 width=12) (actual
time=372.766..380.364 rows=288 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on t (cost=0.00..119319.00 rows=121
width=12) (actual time=268.326..366.228 rows=96 loops=3)
Filter: ((a >= 1923300) AND (a <= 1923600))
Rows Removed by Filter: 3333237
Planning Time: 0.089 ms
Execution Time: 380.434 ms
(8 rows)

And just to compare size:

BRIN 32kB
BRIN multi 136kB
Btree 188MB

[1]: /messages/by-id/459eef3e-48c7-0f5a-8356-992442a78bb6@2ndquadrant.com

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#83Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: John Naylor (#82)
Re: WIP: BRIN multi-range indexes

On Wed, Sep 09, 2020 at 12:04:28PM -0400, John Naylor wrote:

On Sat, Sep 5, 2020 at 7:21 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

OK, here is a rebased version. Most of the breakage was due to changes
to the BRIN sgml docs.

Hi Tomas,

I plan on trying some different queries on different data
distributions to get a sense of when the planner chooses a
multi-minmax index, and whether the choice is good.

Just to start, I used the artificial example in [1], but scaled down a
bit to save time. Config is at the default except for:
shared_buffers = 1GB
random_page_cost = 1.1;
effective_cache_size = 4GB;

create table t (a bigint, b int) with (fillfactor=95);

insert into t select i + 1000*random(), i+1000*random()
from generate_series(1,10000000) s(i);

update t set a = 1, b = 1 where random() < 0.001;
update t set a = 10000000, b = 10000000 where random() < 0.001;

analyze t;

create index on t using brin (a);
CREATE INDEX
Time: 1631.452 ms (00:01.631)

explain analyze select * from t
where a between 1923300::int and 1923600::int;

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on t (cost=16.10..43180.43 rows=291 width=12)
(actual time=217.770..1131.366 rows=288 loops=1)
Recheck Cond: ((a >= 1923300) AND (a <= 1923600))
Rows Removed by Index Recheck: 9999712
Heap Blocks: lossy=56819
-> Bitmap Index Scan on t_a_idx (cost=0.00..16.03 rows=22595
width=0) (actual time=3.054..3.055 rows=568320 loops=1)
Index Cond: ((a >= 1923300) AND (a <= 1923600))
Planning Time: 0.328 ms
Execution Time: 1131.411 ms
(8 rows)

Now add the multi-minmax:

create index on t using brin (a int8_minmax_multi_ops);
CREATE INDEX
Time: 6521.026 ms (00:06.521)

The first interesting thing is, with both BRIN indexes available, the
planner still chooses the conventional BRIN index. Only when I disable
it, does it choose the multi-minmax index:

explain analyze select * from t
where a between 1923300::int and 1923600::int;

QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on t (cost=68.10..43160.86 rows=291 width=12)
(actual time=1.835..4.196 rows=288 loops=1)
Recheck Cond: ((a >= 1923300) AND (a <= 1923600))
Rows Removed by Index Recheck: 22240
Heap Blocks: lossy=128
-> Bitmap Index Scan on t_a_idx1 (cost=0.00..68.03 rows=22523
width=0) (actual time=0.691..0.691 rows=1280 loops=1)
Index Cond: ((a >= 1923300) AND (a <= 1923600))
Planning Time: 0.250 ms
Execution Time: 4.244 ms
(8 rows)

I wonder if this is a clue that something in the costing unfairly
penalizes a multi-minmax index. Maybe not enough to matter in
practice, since I wouldn't expect a user to put different kinds of
index on the same column.

I think this is much more an estimation issue than a costing one. Notice
that in the "regular" BRIN minmax index we have this:

-> Bitmap Index Scan on t_a_idx (cost=0.00..16.03 rows=22595
width=0) (actual time=3.054..3.055 rows=568320 loops=1)

while for the multi-minmax we have this:

-> Bitmap Index Scan on t_a_idx1 (cost=0.00..68.03 rows=22523
width=0) (actual time=0.691..0.691 rows=1280 loops=1)

So yes, the multi-minmax index is costed a bit higher, mostly because
the index is a bit larger. (There's also a tweak to the correlation, but
that does not make much difference because it's just 0.99 vs. 1.0.)

The main difference is that for minmax the bitmap index scan actually
matches ~586k rows (a bit confusing, considering the heap scan has to
process almost 10M rows during recheck). But the multi-minmax only
matches ~1300 rows, with a recheck of 22k.

I'm not sure how to consider this during costing, as we only see these
numbers at execution time. One way would be to also consider "size" of
the ranges (i.e. max-min) vs. range of the whole column. But that's not
something we already have.

I'm not sure how troublesome this issue really is - I don't think people
are very likely to have both minmax and multi-minmax indexes on the same
column.

The second thing is, with parallel seq scan, the query is faster than
a BRIN bitmap scan, with this pathological data distribution, but the
planner won't choose it unless forced to:

set enable_bitmapscan = 'off';
explain analyze select * from t
where a between 1923300::int and 1923600::int;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..120348.10 rows=291 width=12) (actual
time=372.766..380.364 rows=288 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on t (cost=0.00..119319.00 rows=121
width=12) (actual time=268.326..366.228 rows=96 loops=3)
Filter: ((a >= 1923300) AND (a <= 1923600))
Rows Removed by Filter: 3333237
Planning Time: 0.089 ms
Execution Time: 380.434 ms
(8 rows)

I think this is the same root cause - the planner does not realize how
bad the minmax index actually is in this case, so it uses a bit too
optimistic estimate for costing. And then it has to do essentially
seqscan with extra work for bitmap index/heap scan.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#84Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: John Naylor (#82)
Re: WIP: BRIN multi-range indexes

On 2020-Sep-09, John Naylor wrote:

create index on t using brin (a);
CREATE INDEX
Time: 1631.452 ms (00:01.631)

create index on t using brin (a int8_minmax_multi_ops);
CREATE INDEX
Time: 6521.026 ms (00:06.521)

It seems strange that the multi-minmax index takes so much longer to
build. I wonder if there's some obvious part of the algorithm that can
be improved?

The second thing is, with parallel seq scan, the query is faster than
a BRIN bitmap scan, with this pathological data distribution, but the
planner won't choose it unless forced to:

set enable_bitmapscan = 'off';
explain analyze select * from t
where a between 1923300::int and 1923600::int;

This is probably explained by the fact that you likely have the whole
table in shared buffers, or at least in OS cache. I'm not sure if the
costing should necessarily account for this.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#85Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alvaro Herrera (#84)
Re: WIP: BRIN multi-range indexes

On Wed, Sep 09, 2020 at 03:40:41PM -0300, Alvaro Herrera wrote:

On 2020-Sep-09, John Naylor wrote:

create index on t using brin (a);
CREATE INDEX
Time: 1631.452 ms (00:01.631)

create index on t using brin (a int8_minmax_multi_ops);
CREATE INDEX
Time: 6521.026 ms (00:06.521)

It seems strange that the multi-minmax index takes so much longer to
build. I wonder if there's some obvious part of the algorithm that can
be improved?

There are some minor optimizations possible - for example I noticed we
call minmax_multi_get_strategy_procinfo often because it happens in a
loop, and we could easily do it just once. But that saves only about 10%
or so, it's not a ground-breaking optimization.

The main reason for the slowness is that we pass the values one by one
to brin_minmax_multi_add_value - and on each call we need to deserialize
(and then sometimes also serialize) the summary, which may be quite
expensive. The regular minmax does not have this issue, it just swaps
the Datum value and that's it.

I see two possible optimizations - firstly, adding some sort of batch
variant of the add_value function, which would get a bunch of values
instead of just a single one, amortizing the serialization costs.

Another option would be to teach add_value to keep the deserialized
summary somewhere, and then force serialization at the end of the BRIN
page range. The end result would be roughly the same, I think.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#86Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Tomas Vondra (#85)
Re: WIP: BRIN multi-range indexes

On 2020-Sep-09, Tomas Vondra wrote:

There are some minor optimizations possible - for example I noticed we
call minmax_multi_get_strategy_procinfo often because it happens in a
loop, and we could easily do it just once. But that saves only about 10%
or so, it's not a ground-breaking optimization.

Well, I guess this kind of thing should be fixed regardless while we
still know it's there, just to avoid an obvious inefficiency.

The main reason for the slowness is that we pass the values one by one
to brin_minmax_multi_add_value - and on each call we need to deserialize
(and then sometimes also serialize) the summary, which may be quite
expensive. The regular minmax does not have this issue, it just swaps
the Datum value and that's it.

Ah, right, that's more interesting. The original dumb BRIN code
separates BrinMemTuple from BrinTuple so that things can be operated
efficiently in memory. Maybe something similar can be done in this
case, which also sounds like your second suggestion:

Another option would be to teach add_value to keep the deserialized
summary somewhere, and then force serialization at the end of the BRIN
page range. The end result would be roughly the same, I think.

Also, I think you could get a few initial patches pushed soon, since
they look like general improvements rather than specific to multi-range.

On a differen train of thought, I wonder if we shouldn't drop the idea
of there being two minmax opclasses; just have one (still called
"minmax") and have the multi-range version be the v2 of it. We would
still need to keep code to operate on the old one, but if you ever
REINDEX then your index is upgraded to the new one. I see no reason to
keep the dumb minmax version around, assuming the performance is roughly
similar.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#87Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alvaro Herrera (#86)
Re: WIP: BRIN multi-range indexes

On Wed, Sep 09, 2020 at 04:53:30PM -0300, Alvaro Herrera wrote:

On 2020-Sep-09, Tomas Vondra wrote:

There are some minor optimizations possible - for example I noticed we
call minmax_multi_get_strategy_procinfo often because it happens in a
loop, and we could easily do it just once. But that saves only about 10%
or so, it's not a ground-breaking optimization.

Well, I guess this kind of thing should be fixed regardless while we
still know it's there, just to avoid an obvious inefficiency.

Sure. I was just suggesting it's not something that'd make this very
close to plain minmax opclass.

The main reason for the slowness is that we pass the values one by one
to brin_minmax_multi_add_value - and on each call we need to deserialize
(and then sometimes also serialize) the summary, which may be quite
expensive. The regular minmax does not have this issue, it just swaps
the Datum value and that's it.

Ah, right, that's more interesting. The original dumb BRIN code
separates BrinMemTuple from BrinTuple so that things can be operated
efficiently in memory. Maybe something similar can be done in this
case, which also sounds like your second suggestion:

Another option would be to teach add_value to keep the deserialized
summary somewhere, and then force serialization at the end of the BRIN
page range. The end result would be roughly the same, I think.

Well, the patch already has Ranges (memory) and SerializedRanges (disk)
but it's not very clear to me where to stash the in-memory data and
where to make the conversion.

Also, I think you could get a few initial patches pushed soon, since
they look like general improvements rather than specific to multi-range.

Yeah, I agree. I plan to review those once again in a couple days and
then push them.

On a differen train of thought, I wonder if we shouldn't drop the idea
of there being two minmax opclasses; just have one (still called
"minmax") and have the multi-range version be the v2 of it. We would
still need to keep code to operate on the old one, but if you ever
REINDEX then your index is upgraded to the new one. I see no reason to
keep the dumb minmax version around, assuming the performance is roughly
similar.

I'm not a huge fan of that. I think it's unlikely we'll ever make this
new set of oplasses just as fast as the plain minmax, and moreover it
does have some additional requirements (e.g. the distance proc, which
may not make sense for some data types).

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#88Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#87)
Re: WIP: BRIN multi-range indexes

On Wed, Sep 09, 2020 at 10:26:00PM +0200, Tomas Vondra wrote:

On Wed, Sep 09, 2020 at 04:53:30PM -0300, Alvaro Herrera wrote:

On 2020-Sep-09, Tomas Vondra wrote:

There are some minor optimizations possible - for example I noticed we
call minmax_multi_get_strategy_procinfo often because it happens in a
loop, and we could easily do it just once. But that saves only about 10%
or so, it's not a ground-breaking optimization.

Well, I guess this kind of thing should be fixed regardless while we
still know it's there, just to avoid an obvious inefficiency.

Sure. I was just suggesting it's not something that'd make this very
close to plain minmax opclass.

The main reason for the slowness is that we pass the values one by one
to brin_minmax_multi_add_value - and on each call we need to deserialize
(and then sometimes also serialize) the summary, which may be quite
expensive. The regular minmax does not have this issue, it just swaps
the Datum value and that's it.

Ah, right, that's more interesting. The original dumb BRIN code
separates BrinMemTuple from BrinTuple so that things can be operated
efficiently in memory. Maybe something similar can be done in this
case, which also sounds like your second suggestion:

Another option would be to teach add_value to keep the deserialized
summary somewhere, and then force serialization at the end of the BRIN
page range. The end result would be roughly the same, I think.

Well, the patch already has Ranges (memory) and SerializedRanges (disk)
but it's not very clear to me where to stash the in-memory data and
where to make the conversion.

I've spent a bit of time experimenting with this. My idea was to allow
keeping an "expanded" version of the summary somewhere. As the addValue
function only receives BrinValues I guess one option would be to just
add bv_mem_values field. Or do you have a better idea?

Of course, more would need to be done:

1) We'd need to also pass the right memory context (bt_context seems
like the right thing, but that's not something addValue sees now).

2) We'd also need to specify some sort of callback that serializes the
in-memory value into bt_values. That's not something addValue can do,
because it doesn't know whether it's the last value in the range etc. I
guess one option would be to add yet another support proc, but I guess a
simple callback would be enough.

I've hacked together an experimental version of this to see how much
would it help, and it reduces the duration from ~4.6s to ~3.3s. Which is
nice, but plain minmax is ~1.1s. I suppose there's room for further
improvements in compare_combine_ranges/reduce_combine_ranges and so on,
but I still think there'll always be a gap compared to plain minmax.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#89Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Tomas Vondra (#88)
Re: WIP: BRIN multi-range indexes

On 2020-Sep-10, Tomas Vondra wrote:

I've spent a bit of time experimenting with this. My idea was to allow
keeping an "expanded" version of the summary somewhere. As the addValue
function only receives BrinValues I guess one option would be to just
add bv_mem_values field. Or do you have a better idea?

Maybe it's okay to pass the BrinMemTuple to the add_value function, and
keep something there. Or maybe that's pointless and just a new field in
BrinValues is okay.

Of course, more would need to be done:

1) We'd need to also pass the right memory context (bt_context seems
like the right thing, but that's not something addValue sees now).

You could use GetMemoryChunkContext() for that.

2) We'd also need to specify some sort of callback that serializes the
in-memory value into bt_values. That's not something addValue can do,
because it doesn't know whether it's the last value in the range etc. I
guess one option would be to add yet another support proc, but I guess a
simple callback would be enough.

Hmm.

I've hacked together an experimental version of this to see how much
would it help, and it reduces the duration from ~4.6s to ~3.3s. Which is
nice, but plain minmax is ~1.1s. I suppose there's room for further
improvements in compare_combine_ranges/reduce_combine_ranges and so on,
but I still think there'll always be a gap compared to plain minmax.

The main reason I'm talking about desupporting plain minmax is that,
even if it's amazingly fast, it loses quite quickly in real-world cases
because of loss of correlation. Minmax's build time is pretty much
determined by speed at which you can seqscan the table. I don't think
we lose much if we add overhead in order to create an index that is 100x
more useful.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#90John Naylor
john.naylor@2ndquadrant.com
In reply to: Tomas Vondra (#88)
Re: WIP: BRIN multi-range indexes

Ok, here's an attempt at a somewhat more natural test, to see what
happens after bulk updates and deletes, followed by more inserts. The
short version is that multi-minmax is resilient to a change that
causes a 4x degradation for simple minmax.

shared_buffers = 1GB
random_page_cost = 1.1
effective_cache_size = 4GB
work_mem = 64MB
maintenance_work_mem = 512MB

create unlogged table iot (
id bigint generated by default as identity primary key,
num double precision not null,
create_dt timestamptz not null,
stuff text generated always as (md5(id::text)) stored
)
with (fillfactor = 95);

insert into iot (num, create_dt)
select random(), x
from generate_series(
'2020-01-01 0:00'::timestamptz,
'2020-01-01 0:00'::timestamptz +'49000999 seconds'::interval,
'2 seconds'::interval) x;

INSERT 0 24500500

(01:18s, 2279 MB)

-- done in separate tests so the planner can choose each in turn
create index cd_single on iot using brin(create_dt);
6.7s
create index cd_multi on iot using brin(create_dt timestamptz_minmax_multi_ops);
34s

vacuum analyze;

-- aggregate February
-- single minmax and multi-minmax same plan and same Heap Blocks
below, so only one plan shown
-- query times between the opclasses within noise of variation

explain analyze select date_trunc('day', create_dt), avg(num)
from iot
where create_dt >= '2020-02-01 0:00' and create_dt < '2020-03-01 0:00'
group by 1;

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
HashAggregate (cost=357664.79..388181.83 rows=1232234 width=16)
(actual time=559.805..561.649 rows=29 loops=1)
Group Key: date_trunc('day'::text, create_dt)
Planned Partitions: 4 Batches: 1 Memory Usage: 24601kB
-> Bitmap Heap Scan on iot (cost=323.74..313622.05 rows=1232234
width=16) (actual time=1.787..368.256 rows=1252800 loops=1)
Recheck Cond: ((create_dt >= '2020-02-01
00:00:00-04'::timestamp with time zone) AND (create_dt < '2020-03-01
00:00:00-04'::timestamp with time zone))
Rows Removed by Index Recheck: 15936
Heap Blocks: lossy=15104
-> Bitmap Index Scan on cd_single (cost=0.00..15.68
rows=1236315 width=0) (actual time=0.933..0.934 rows=151040 loops=1)
Index Cond: ((create_dt >= '2020-02-01
00:00:00-04'::timestamp with time zone) AND (create_dt < '2020-03-01
00:00:00-04'::timestamp with time zone))
Planning Time: 0.118 ms
Execution Time: 568.653 ms
(11 rows)

-- delete first month and hi/lo values to create some holes in the table
delete from iot
where create_dt < '2020-02-01 0:00'::timestamptz;

DELETE 1339200

delete from iot
where num < 0.05
or num > 0.95;

DELETE 2316036

vacuum analyze iot;

-- add add back first month, but with double density (1s step rather
than 2s) so it spills over into other parts of the table, causing more
block ranges to have a lower bound with this month.

insert into iot (num, create_dt)
select random(), x
from generate_series(
'2020-01-01 0:00'::timestamptz,
'2020-01-31 23:59'::timestamptz,
'1 second'::interval) x;

INSERT 0 2678341

vacuum analyze;

-- aggregate February again

explain analyze select date_trunc('day', create_dt), avg(num)
from iot
where create_dt >= '2020-02-01 0:00' and create_dt < '2020-03-01 0:00'
group by 1;

-- simple minmax:

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
HashAggregate (cost=354453.63..383192.38 rows=1160429 width=16)
(actual time=2375.075..2376.982 rows=29 loops=1)
Group Key: date_trunc('day'::text, create_dt)
Planned Partitions: 4 Batches: 1 Memory Usage: 24601kB
-> Bitmap Heap Scan on iot (cost=305.85..312977.36 rows=1160429
width=16) (actual time=8.162..2201.547 rows=1127668 loops=1)
Recheck Cond: ((create_dt >= '2020-02-01
00:00:00-04'::timestamp with time zone) AND (create_dt < '2020-03-01
00:00:00-04'::timestamp with time zone))
Rows Removed by Index Recheck: 12278985
Heap Blocks: lossy=159616
-> Bitmap Index Scan on cd_single (cost=0.00..15.74
rows=1206496 width=0) (actual time=7.177..7.178 rows=1596160 loops=1)
Index Cond: ((create_dt >= '2020-02-01
00:00:00-04'::timestamp with time zone) AND (create_dt < '2020-03-01
00:00:00-04'::timestamp with time zone))
Planning Time: 0.117 ms
Execution Time: 2383.685 ms
(11 rows)

-- multi minmax:

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
HashAggregate (cost=354089.57..382932.46 rows=1164634 width=16)
(actual time=535.773..537.731 rows=29 loops=1)
Group Key: date_trunc('day'::text, create_dt)
Planned Partitions: 4 Batches: 1 Memory Usage: 24601kB
-> Bitmap Heap Scan on iot (cost=376.07..312463.00 rows=1164634
width=16) (actual time=3.731..363.116 rows=1127117 loops=1)
Recheck Cond: ((create_dt >= '2020-02-01
00:00:00-04'::timestamp with time zone) AND (create_dt < '2020-03-01
00:00:00-04'::timestamp with time zone))
Rows Removed by Index Recheck: 141619
Heap Blocks: lossy=15104
-> Bitmap Index Scan on cd_multi (cost=0.00..84.92
rows=1166823 width=0) (actual time=3.048..3.048 rows=151040 loops=1)
Index Cond: ((create_dt >= '2020-02-01
00:00:00-04'::timestamp with time zone) AND (create_dt < '2020-03-01
00:00:00-04'::timestamp with time zone))
Planning Time: 0.117 ms
Execution Time: 545.246 ms
(11 rows)

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#91Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Alvaro Herrera (#89)
10 attachment(s)
Re: WIP: BRIN multi-range indexes

On Thu, Sep 10, 2020 at 05:01:37PM -0300, Alvaro Herrera wrote:

On 2020-Sep-10, Tomas Vondra wrote:

I've spent a bit of time experimenting with this. My idea was to allow
keeping an "expanded" version of the summary somewhere. As the addValue
function only receives BrinValues I guess one option would be to just
add bv_mem_values field. Or do you have a better idea?

Maybe it's okay to pass the BrinMemTuple to the add_value function, and
keep something there. Or maybe that's pointless and just a new field in
BrinValues is okay.

OK. I don't like changing the add_value API very much, so for the
experimental version I simply added three new fields into the BrinValues
struct - the deserialized value, serialization callback and the memory
context. This seems to be working good enough for a WIP version.

With the original (non-batched) patch version a build of an index took
about 4s. With the minmax_multi_get_strategy_procinfo optimization and
batch build it now takes ~2.6s, which is quite nice. It's still ~2.5x as
much compared to plain minmax though.

I think there's still room for a bit more improvement (in how we merge
the ranges etc.) and maybe we can get to ~2s or something like that.

Of course, more would need to be done:

1) We'd need to also pass the right memory context (bt_context seems
like the right thing, but that's not something addValue sees now).

You could use GetMemoryChunkContext() for that.

Maybe, although I prefer to just pass the memory context explicitly.

2) We'd also need to specify some sort of callback that serializes the
in-memory value into bt_values. That's not something addValue can do,
because it doesn't know whether it's the last value in the range etc. I
guess one option would be to add yet another support proc, but I guess a
simple callback would be enough.

Hmm.

I added a simple serialization callback. It works but it's a bit weird
that twe have most functions defined as support procedures, and then
this extra C callback.

I've hacked together an experimental version of this to see how much
would it help, and it reduces the duration from ~4.6s to ~3.3s. Which is
nice, but plain minmax is ~1.1s. I suppose there's room for further
improvements in compare_combine_ranges/reduce_combine_ranges and so on,
but I still think there'll always be a gap compared to plain minmax.

The main reason I'm talking about desupporting plain minmax is that,
even if it's amazingly fast, it loses quite quickly in real-world cases
because of loss of correlation. Minmax's build time is pretty much
determined by speed at which you can seqscan the table. I don't think
we lose much if we add overhead in order to create an index that is 100x
more useful.

I understand. I just feel a bit uneasy about replacing an index with
something that may or may not be better for a certain use case. I mean,
if you have data set for which regular minmax works fine, wouldn't you
be annoyed if we just switched it for something slower?

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function-a-20200911.patchtext/plain; charset=us-asciiDownload
From d66724b5472fb7afc40d0b7e916b128bf675e9de Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 13 Sep 2019 18:34:39 +0200
Subject: [PATCH 01/10] Pass all keys to BRIN consistent function at once

---
 src/backend/access/brin/brin.c           | 126 ++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 164 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 116 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 293 insertions(+), 121 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1f72562c60..6777d48faf 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,53 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +520,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,34 +530,19 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (!nkeys[attno - 1])
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
@@ -519,12 +554,43 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					 * the range as a whole, so break out of the loop as soon
 					 * as a false return value is obtained.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/* Check keys one by one */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..8968886ff5 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,103 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		matches;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = inclusion_consistent_key(bdesc, column, key, colloid);
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_BOOL(matches);
+}
+
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +376,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +437,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +458,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +477,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +512,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +522,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..1219a3a2ab 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,99 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
-	FmgrInfo   *finfo;
+	int			keyno;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = minmax_consistent_key(bdesc, column, key, colloid);
+
+		/* found non-matching key */
+		if (!matches)
+			break;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(matches);
+}
+
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +283,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 687509ba92..925b262b60 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8104,7 +8104,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8120,7 +8120,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.25.4

0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20200911.patchtext/plain; charset=us-asciiDownload
From 6692845cd0f8723c66db5d19afe5add61eb65b6c Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:05:39 +0200
Subject: [PATCH 02/10] Move IS NOT NULL checks to bringetbitmap

---
 src/backend/access/brin/brin.c           | 116 ++++++++++++++++++++---
 src/backend/access/brin/brin_inclusion.c |  62 +-----------
 src/backend/access/brin/brin_minmax.c    |  62 +-----------
 3 files changed, 109 insertions(+), 131 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 6777d48faf..caf7b62688 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,8 +389,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +417,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -440,14 +445,12 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 							  keyattno - 1)->attcollation));
 
 		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -455,9 +458,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -544,6 +561,83 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys, and
+					 * if we're violating them. In that case we can terminate
+					 * early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					{
+						ScanKey	key = nullkeys[attno - 1][keyno];
+
+						Assert(key->sk_attno == bval->bv_attno);
+
+						/* interrupt the loop as soon as we find a mismatch */
+						if (!addrange)
+							break;
+
+						/* handle IS NULL/IS NOT NULL tests */
+						if (key->sk_flags & SK_ISNULL)
+						{
+							/* IS NULL scan key, but range has no NULLs */
+							if (key->sk_flags & SK_SEARCHNULL)
+							{
+								if (!bval->bv_allnulls && !bval->bv_hasnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * For IS NOT NULL, we can only skip ranges that are
+							 * known to have only nulls.
+							 */
+							if (key->sk_flags & SK_SEARCHNOTNULL)
+							{
+								if (bval->bv_allnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * Neither IS NULL nor IS NOT NULL was used; assume all
+							 * indexable operators are strict and thus return false
+							 * with NULL value in the scan key.
+							 */
+							addrange = false;
+						}
+					}
+
+					/*
+					 * If any of the IS [NOT] NULL keys failed, the page range as
+					 * a whole can't pass. So terminate the loop.
+					 */
+					if (!addrange)
+						break;
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed. If
+					 * there are no regular scan keys, we're done - the page range
+					 * matches. If there are regular keys, but the page range is
+					 * marked as 'all nulls' it can't possibly pass (we're assuming
+					 * the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 8968886ff5..22edc6b46f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -265,63 +265,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
 	bool		matches;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -333,9 +276,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = inclusion_consistent_key(bdesc, column, key, colloid);
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 1219a3a2ab..7a7bd21cec 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -153,63 +153,6 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	matches = true;
 
@@ -217,9 +160,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = minmax_consistent_key(bdesc, column, key, colloid);
 
-- 
2.25.4

0003-Move-processing-of-NULLs-from-BRIN-support--20200911.patchtext/plain; charset=us-asciiDownload
From c65947755bb7d5ef9c459b57a5ac246742b6d608 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:56:00 +0200
Subject: [PATCH 03/10] Move processing of NULLs from BRIN support functions

---
 src/backend/access/brin/brin.c           | 260 ++++++++++++++---------
 src/backend/access/brin/brin_inclusion.c |  44 +---
 src/backend/access/brin/brin_minmax.c    |  41 +---
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 169 insertions(+), 179 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index caf7b62688..a9c44c0b82 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -359,6 +337,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -562,69 +541,31 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
-					 * First check if there are any IS [NOT] NULL scan keys, and
-					 * if we're violating them. In that case we can terminate
-					 * early, without invoking the support function.
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
 					 *
 					 * As there may be more keys, we can only detemine mismatch
 					 * within this loop.
 					 */
-					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
 					{
-						ScanKey	key = nullkeys[attno - 1][keyno];
-
-						Assert(key->sk_attno == bval->bv_attno);
-
-						/* interrupt the loop as soon as we find a mismatch */
-						if (!addrange)
-							break;
-
-						/* handle IS NULL/IS NOT NULL tests */
-						if (key->sk_flags & SK_ISNULL)
-						{
-							/* IS NULL scan key, but range has no NULLs */
-							if (key->sk_flags & SK_SEARCHNULL)
-							{
-								if (!bval->bv_allnulls && !bval->bv_hasnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * For IS NOT NULL, we can only skip ranges that are
-							 * known to have only nulls.
-							 */
-							if (key->sk_flags & SK_SEARCHNOTNULL)
-							{
-								if (bval->bv_allnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * Neither IS NULL nor IS NOT NULL was used; assume all
-							 * indexable operators are strict and thus return false
-							 * with NULL value in the scan key.
-							 */
-							addrange = false;
-						}
-					}
-
-					/*
-					 * If any of the IS [NOT] NULL keys failed, the page range as
-					 * a whole can't pass. So terminate the loop.
-					 */
-					if (!addrange)
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
 						break;
+					}
 
 					/*
-					 * So either there are no IS [NOT] NULL keys, or all passed. If
-					 * there are no regular scan keys, we're done - the page range
-					 * matches. If there are regular keys, but the page range is
-					 * marked as 'all nulls' it can't possibly pass (we're assuming
-					 * the operators are strict).
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
 					 */
 
 					/* No regular scan keys - page range as a whole passes. */
@@ -772,7 +713,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -801,25 +741,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1598,6 +1521,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1651,3 +1607,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 22edc6b46f..59503b6f68 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -510,37 +500,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 7a7bd21cec..8882eec12c 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -245,34 +235,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

0004-BRIN-bloom-indexes-20200911.patchtext/plain; charset=us-asciiDownload
From a7c8e89eb5be0c83e3072c6788007ae71cbe19dc Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sat, 5 Sep 2020 23:54:39 +0200
Subject: [PATCH 04/10] BRIN bloom indexes

---
 doc/src/sgml/brin.sgml                   | 173 ++++
 doc/src/sgml/ref/create_index.sgml       |  31 +
 src/backend/access/brin/Makefile         |   1 +
 src/backend/access/brin/brin_bloom.c     | 980 +++++++++++++++++++++++
 src/include/access/brin.h                |   2 +
 src/include/access/brin_internal.h       |   4 +
 src/include/catalog/pg_amop.dat          | 165 ++++
 src/include/catalog/pg_amproc.dat        | 430 ++++++++++
 src/include/catalog/pg_opclass.dat       |  69 ++
 src/include/catalog/pg_opfamily.dat      |  36 +
 src/include/catalog/pg_proc.dat          |  20 +
 src/test/regress/expected/brin_bloom.out | 456 +++++++++++
 src/test/regress/expected/opr_sanity.out |   3 +-
 src/test/regress/expected/psql.out       |   3 +-
 src/test/regress/parallel_schedule       |   5 +
 src/test/regress/serial_schedule         |   1 +
 src/test/regress/sql/brin_bloom.sql      | 404 ++++++++++
 17 files changed, 2781 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..9b1d94b772 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -353,6 +447,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +461,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +475,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +489,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +503,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +898,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 33aa64e81d..9c90d451a7 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -555,6 +555,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..b119e4e264
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,980 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i,
+			nvalues;
+	Size	len;
+	uint32 *values;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* TODO optimization: sort only the unsorted part, then merge */
+	pg_qsort(values, filter->nvalues, sizeof(uint32), cmp_uint32);
+
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (values[i] != values[nvalues - 1])
+			values[nvalues++] = values[i];
+	}
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..af6891e348 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2002,6 +2065,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2096,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2118,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2140,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2182,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2204,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2355,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2410,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2432,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2488,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2510,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2576,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 37b580883f..c24a80545a 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -770,6 +770,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -783,6 +801,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -796,6 +832,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -897,6 +951,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -910,6 +1016,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -922,6 +1046,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -984,6 +1125,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -998,6 +1178,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1012,6 +1212,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1025,6 +1245,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1059,6 +1297,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1072,6 +1330,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1179,6 +1455,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1193,6 +1525,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1207,6 +1559,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1247,6 +1619,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1260,6 +1652,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1295,6 +1705,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..16d6111ab6 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,127 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +386,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..7f733aeab1 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,86 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 925b262b60..c00e4ece94 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8127,6 +8127,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 1b3c146e4c..dca8e9eb34 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2034,6 +2034,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2097,7 +2098,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index daac0ff49d..72c7598c89 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4989,8 +4989,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..b49239b1d0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..abce8e180a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -107,6 +107,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.25.4

0005-add-special-pg_brin_bloom_summary-data-type-20200911.patchtext/plain; charset=us-asciiDownload
From 8b24542a86ab5ac259ac3de489e3b248de265822 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 6 Aug 2020 17:56:22 +0200
Subject: [PATCH 05/10] add special pg_brin_bloom_summary data type

---
 src/backend/access/brin/brin_bloom.c      | 91 ++++++++++++++++++++++-
 src/include/catalog/pg_proc.dat           | 14 ++++
 src/include/catalog/pg_type.dat           |  7 ++
 src/test/regress/expected/type_sanity.out |  7 +-
 4 files changed, 115 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index b119e4e264..e7ca100821 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -647,7 +647,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
-	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+	result->oi_typcache[0] = lookup_type_cache(BRINBLOOMSUMMARYOID, 0);
 
 	PG_RETURN_POINTER(result);
 }
@@ -978,3 +978,92 @@ brin_bloom_options(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c00e4ece94..dc17d4ce38 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10993,3 +10993,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index b2cec07416..a41c2e5418 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -631,3 +631,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034', oid_symbol => 'BRINBLOOMSUMMARYOID',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
-- 
2.25.4

0006-BRIN-minmax-multi-indexes-20200911.patchtext/plain; charset=us-asciiDownload
From a4def85da8b00c126031668c207f6541d2f1d9a6 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 6 Sep 2020 00:10:44 +0200
Subject: [PATCH 06/10] BRIN minmax-multi indexes

---
 doc/src/sgml/brin.sgml                      |  252 ++-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2172 +++++++++++++++++++
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/transam.h                |    8 +-
 src/include/catalog/pg_amop.dat             |  545 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   65 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 ++++
 17 files changed, 4532 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 9b1d94b772..46300dada5 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -447,6 +564,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -461,6 +587,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -475,6 +610,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -489,6 +633,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -503,6 +656,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -517,6 +679,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -649,13 +820,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -952,6 +1124,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 9c90d451a7..f6b8fbd1d6 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -586,6 +586,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 64.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..a8ff28a98d
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2172 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+static Ranges *range_deserialize(SerializedRanges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (attr->attlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (attr->attlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(attr->attlen > 0);
+		len += nvalues * attr->attlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized,
+				AttrNumber attno, Form_pg_attribute attr)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count_values(cranges, ncranges) <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		column->bv_allnulls = false;
+		modified = true;
+	}
+	else
+	{
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized, attno, attr);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+	if (modified)
+	{
+		SerializedRanges *s = range_serialize(ranges, attno, attr);
+		column->bv_values[0] = PointerGetDatum(s);
+
+		/*
+		 * XXX pfree must happen after range_serialize, because the Ranges value
+		 * may reference the original serialized value.
+		 */
+		if (serialized)
+			pfree(serialized);
+	}
+
+	pfree(ranges);
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+	Form_pg_attribute attr;
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized, attno, attr);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a, attno, attr);
+	ranges_b = range_deserialize(serialized_b, attno, attr);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attno, attr));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 2f1f144db4..b0993f8ac5 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -165,14 +165,14 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  *		when the .dat files in src/include/catalog/ do not specify an OID
  *		for a catalog entry that requires one.
  *
- *		OIDS 12000-16383 are reserved for assignment during initdb
- *		using the OID generator.  (We start the generator at 12000.)
+ *		OIDS 13000-16383 are reserved for assignment during initdb
+ *		using the OID generator.  (We start the generator at 13000.)
  *
  *		OIDs beginning at 16384 are assigned from the OID generator
  *		during normal multiuser operation.  (We force the generator up to
  *		16384 as soon as we are in normal operation.)
  *
- * The choices of 8000, 10000 and 12000 are completely arbitrary, and can be
+ * The choices of 8000, 10000 and 13000 are completely arbitrary, and can be
  * moved if we run low on OIDs in any category.  Changing the macros below,
  * and updating relevant documentation (see bki.sgml and RELEASE_CHANGES),
  * should be sufficient to do this.  Moving the 16384 boundary between
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index af6891e348..31ec56766a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1900,6 +1900,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1977,6 +2123,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -1999,6 +2162,23 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2065,6 +2245,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2096,6 +2342,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2118,6 +2381,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2140,6 +2420,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2204,6 +2501,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2355,6 +2669,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2410,6 +2870,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2432,6 +2909,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2488,6 +2982,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2510,6 +3021,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2576,6 +3104,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index c24a80545a..317c9475f8 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -951,6 +951,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1046,6 +1192,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1075,6 +1238,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1125,6 +1305,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1178,6 +1432,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1212,6 +1486,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1245,6 +1539,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1330,6 +1644,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1455,6 +1788,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1525,6 +2022,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1559,6 +2076,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1619,6 +2156,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1650,7 +2207,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1705,6 +2283,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 16d6111ab6..92b3a577ea 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,38 +307,59 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -345,36 +375,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -386,6 +434,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -395,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -404,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 7f733aeab1..8bbf753f65 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,24 +220,36 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -240,10 +260,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -252,12 +276,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dc17d4ce38..1936e06537 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8110,6 +8110,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 72c7598c89..f855633634 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4986,12 +4986,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b49239b1d0..a933db5456 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index abce8e180a..3f05a7061f 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.25.4

0007-add-special-pg_brin_minmax_multi_summary-da-20200911.patchtext/plain; charset=us-asciiDownload
From df52a663305f1f900801925e2914a4c355177427 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 6 Aug 2020 18:20:28 +0200
Subject: [PATCH 07/10] add special pg_brin_minmax_multi_summary data type

---
 src/backend/access/brin/brin_minmax_multi.c | 260 +++++++++++++++++---
 src/include/catalog/pg_proc.dat             |  15 ++
 src/include/catalog/pg_type.dat             |   7 +
 src/test/regress/expected/type_sanity.out   |   7 +-
 4 files changed, 251 insertions(+), 38 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index a8ff28a98d..1a1ff47223 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -59,6 +59,7 @@
 #include "access/htup_details.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_amop.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/date.h"
 #include "utils/datum.h"
@@ -165,6 +166,9 @@ typedef struct SerializedRanges
 	/* varlena header (do not touch directly!) */
 	int32	vl_len_;
 
+	/* type of values stored in the data array */
+	Oid		typid;
+
 	/* (2*nranges + nvalues) <= maxvalues */
 	int		nranges;	/* number of ranges in the array (stored) */
 	int		nvalues;	/* number of values in the data array (all) */
@@ -174,11 +178,9 @@ typedef struct SerializedRanges
 	char	data[FLEXIBLE_ARRAY_MEMBER];
 } SerializedRanges;
 
-static SerializedRanges *range_serialize(Ranges *range,
-				AttrNumber attno, Form_pg_attribute attr);
+static SerializedRanges *range_serialize(Ranges *range, Oid typid);
 
-static Ranges *range_deserialize(SerializedRanges *range,
-				AttrNumber attno, Form_pg_attribute attr);
+static Ranges *range_deserialize(SerializedRanges *range);
 
 /* Cache for support and strategy procesures. */
 
@@ -223,11 +225,13 @@ minmax_multi_init(int maxvalues)
  * in the in-memory value array.
  */
 static SerializedRanges *
-range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
+range_serialize(Ranges *range, Oid typid)
 {
 	Size	len;
 	int		nvalues;
 	SerializedRanges *serialized;
+	int		typlen;
+	bool	typbyval;
 
 	int		i;
 	char   *ptr;
@@ -242,6 +246,9 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 
 	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
 
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
 	/* header is always needed */
 	len = offsetof(SerializedRanges,data);
 
@@ -251,7 +258,7 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 	 * (attlen * nvalues) and we're done. For variable-length by-reference
 	 * types we need to actually walk all the values and sum the lengths.
 	 */
-	if (attr->attlen == -1)	/* varlena */
+	if (typlen == -1)	/* varlena */
 	{
 		int i;
 		for (i = 0; i < nvalues; i++)
@@ -259,7 +266,7 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 			len += VARSIZE_ANY(range->values[i]);
 		}
 	}
-	else if (attr->attlen == -2)	/* cstring */
+	else if (typlen == -2)	/* cstring */
 	{
 		int i;
 		for (i = 0; i < nvalues; i++)
@@ -270,8 +277,8 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 	}
 	else /* fixed-length types (even by-reference) */
 	{
-		Assert(attr->attlen > 0);
-		len += nvalues * attr->attlen;
+		Assert(typlen > 0);
+		len += nvalues * typlen;
 	}
 
 	/*
@@ -281,6 +288,7 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 	serialized = (SerializedRanges *) palloc0(len);
 	SET_VARSIZE(serialized, len);
 
+	serialized->typid = typid;
 	serialized->nranges = range->nranges;
 	serialized->nvalues = range->nvalues;
 	serialized->maxvalues = range->maxvalues;
@@ -293,23 +301,23 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
 
 	for (i = 0; i < nvalues; i++)
 	{
-		if (attr->attbyval)	/* simple by-value data types */
+		if (typbyval)	/* simple by-value data types */
 		{
-			memcpy(ptr, &range->values[i], attr->attlen);
-			ptr += attr->attlen;
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
 		}
-		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		else if (typlen > 0)	/* fixed-length by-ref types */
 		{
-			memcpy(ptr, DatumGetPointer(range->values[i]), attr->attlen);
-			ptr += attr->attlen;
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
 		}
-		else if (attr->attlen == -1)	/* varlena */
+		else if (typlen == -1)	/* varlena */
 		{
 			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
 			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
 			ptr += tmp;
 		}
-		else if (attr->attlen == -2)	/* cstring */
+		else if (typlen == -2)	/* cstring */
 		{
 			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
 			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
@@ -334,12 +342,13 @@ range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
  * in the in-memory value array.
  */
 static Ranges *
-range_deserialize(SerializedRanges *serialized,
-				AttrNumber attno, Form_pg_attribute attr)
+range_deserialize(SerializedRanges *serialized)
 {
 	int		i,
 			nvalues;
 	char   *ptr;
+	bool	typbyval;
+	int		typlen;
 
 	Ranges *range;
 
@@ -358,6 +367,9 @@ range_deserialize(SerializedRanges *serialized,
 	range->nvalues = serialized->nvalues;
 	range->maxvalues = serialized->maxvalues;
 
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
 	/*
 	 * And now deconstruct the values into Datum array. We don't need
 	 * to copy the values and will instead just point the values to the
@@ -367,23 +379,23 @@ range_deserialize(SerializedRanges *serialized,
 
 	for (i = 0; i < nvalues; i++)
 	{
-		if (attr->attbyval)	/* simple by-value data types */
+		if (typbyval)	/* simple by-value data types */
 		{
-			memcpy(&range->values[i], ptr, attr->attlen);
-			ptr += attr->attlen;
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
 		}
-		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		else if (typlen > 0)	/* fixed-length by-ref types */
 		{
 			/* no copy, just set the value to the pointer */
 			range->values[i] = PointerGetDatum(ptr);
-			ptr += attr->attlen;
+			ptr += typlen;
 		}
-		else if (attr->attlen == -1)	/* varlena */
+		else if (typlen == -1)	/* varlena */
 		{
 			range->values[i] = PointerGetDatum(ptr);
 			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
 		}
-		else if (attr->attlen == -2)	/* cstring */
+		else if (typlen == -2)	/* cstring */
 		{
 			range->values[i] = PointerGetDatum(ptr);
 			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
@@ -1287,7 +1299,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
-	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+	result->oi_typcache[0] = lookup_type_cache(BRINMINMAXMULTISUMMARYOID, 0);
 
 	PG_RETURN_POINTER(result);
 }
@@ -1732,7 +1744,7 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	else
 	{
 		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-		ranges = range_deserialize(serialized, attno, attr);
+		ranges = range_deserialize(serialized);
 	}
 
 	/*
@@ -1743,7 +1755,7 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 
 	if (modified)
 	{
-		SerializedRanges *s = range_serialize(ranges, attno, attr);
+		SerializedRanges *s = range_serialize(ranges, attr->atttypid);
 		column->bv_values[0] = PointerGetDatum(s);
 
 		/*
@@ -1782,13 +1794,11 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
 	int			keyno;
 	int			rangeno;
 	int			i;
-	Form_pg_attribute attr;
 
 	attno = column->bv_attno;
-	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
 	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-	ranges = range_deserialize(serialized, attno, attr);
+	ranges = range_deserialize(serialized);
 
 	/* inspect the ranges, and for each one evaluate the scan keys */
 	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
@@ -1975,8 +1985,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
 	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
 
-	ranges_a = range_deserialize(serialized_a, attno, attr);
-	ranges_b = range_deserialize(serialized_b, attno, attr);
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
 
 	/* make sure neither of the ranges is NULL */
 	Assert(ranges_a && ranges_b);
@@ -2046,7 +2056,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 
 	/* cleanup and update the serialized value */
 	pfree(serialized_a);
-	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attno, attr));
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attr->atttypid));
 
 	PG_RETURN_VOID();
 }
@@ -2170,3 +2180,183 @@ brin_minmax_multi_options(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
+
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 1936e06537..77735c3abe 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11072,3 +11072,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index a41c2e5418..c189b35a3d 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -638,3 +638,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039', oid_symbol => 'BRINMINMAXMULTISUMMARYOID',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 97bf9797de..55a30a6b47 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
-- 
2.25.4

0008-tweak-costing-for-bloom-minmax-multi-indexe-20200911.patchtext/plain; charset=us-asciiDownload
From e6feeb0f9459a876510238579aff7cc4a0ebc400 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Fri, 7 Aug 2020 15:53:55 +0200
Subject: [PATCH 08/10] tweak costing for bloom/minmax-multi indexes

---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index e7ca100821..f64381b657 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -643,6 +643,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 1a1ff47223..23a8751b1f 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1295,6 +1295,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 00c7afc66f..c00265a66d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7350,7 +7351,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7372,6 +7374,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7405,6 +7408,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7475,6 +7489,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

0009-WIP-batch-build-20200911.patchtext/plain; charset=us-asciiDownload
From 565d9c53858d3550e282f070f9d8e92b582535ed Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 10 Sep 2020 19:58:12 +0200
Subject: [PATCH 09/10] WIP: batch build

---
 src/backend/access/brin/brin_minmax_multi.c | 58 ++++++++++++++-------
 src/backend/access/brin/brin_tuple.c        | 17 ++++++
 src/include/access/brin_tuple.h             |  4 ++
 3 files changed, 61 insertions(+), 18 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 23a8751b1f..7450bffc11 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -145,6 +145,8 @@ typedef struct MinMaxOptions
  */
 typedef struct Ranges
 {
+	Oid		typid;
+
 	/* (2*nranges + nvalues) <= maxvalues */
 	int		nranges;	/* number of ranges in the array (stored) */
 	int		nvalues;	/* number of values in the data array (all) */
@@ -178,7 +180,7 @@ typedef struct SerializedRanges
 	char	data[FLEXIBLE_ARRAY_MEMBER];
 } SerializedRanges;
 
-static SerializedRanges *range_serialize(Ranges *range, Oid typid);
+static SerializedRanges *range_serialize(Ranges *range);
 
 static Ranges *range_deserialize(SerializedRanges *range);
 
@@ -225,11 +227,12 @@ minmax_multi_init(int maxvalues)
  * in the in-memory value array.
  */
 static SerializedRanges *
-range_serialize(Ranges *range, Oid typid)
+range_serialize(Ranges *range)
 {
 	Size	len;
 	int		nvalues;
 	SerializedRanges *serialized;
+	Oid		typid;
 	int		typlen;
 	bool	typbyval;
 
@@ -246,6 +249,7 @@ range_serialize(Ranges *range, Oid typid)
 
 	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
 
+	typid = range->typid;
 	typbyval = get_typbyval(typid);
 	typlen = get_typlen(typid);
 
@@ -366,6 +370,7 @@ range_deserialize(SerializedRanges *serialized)
 	range->nranges = serialized->nranges;
 	range->nvalues = serialized->nvalues;
 	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
 
 	typbyval = get_typbyval(serialized->typid);
 	typlen = get_typlen(serialized->typid);
@@ -1699,6 +1704,14 @@ brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
 	PG_RETURN_FLOAT8(delta);
 }
 
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
 static int
 brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
 {
@@ -1732,20 +1745,43 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
 	/*
 	 * If this is the first non-null value, we need to initialize the range
 	 * list. Otherwise just extract the existing range list from BrinValues.
 	 */
 	if (column->bv_allnulls)
 	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
 		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
 		column->bv_allnulls = false;
 		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
 	}
-	else
+	else if (!ranges)
 	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
 		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
 		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
 	}
 
 	/*
@@ -1754,20 +1790,6 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	 */
 	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
 
-	if (modified)
-	{
-		SerializedRanges *s = range_serialize(ranges, attr->atttypid);
-		column->bv_values[0] = PointerGetDatum(s);
-
-		/*
-		 * XXX pfree must happen after range_serialize, because the Ranges value
-		 * may reference the original serialized value.
-		 */
-		if (serialized)
-			pfree(serialized);
-	}
-
-	pfree(ranges);
 
 	PG_RETURN_BOOL(modified);
 }
@@ -2057,7 +2079,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 
 	/* cleanup and update the serialized value */
 	pfree(serialized_a);
-	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attr->atttypid));
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
 
 	PG_RETURN_VOID();
 }
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 46e6b23c87..8bdf837070 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -138,6 +138,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		for (datumno = 0;
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
@@ -398,6 +406,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -477,6 +490,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index a9ccc3995b..064a93d09b 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
-- 
2.25.4

0010-WIP-simplify-reduce_combine_ranges-20200911.patchtext/plain; charset=us-asciiDownload
From 830cc627bd9359a5d0f5b18e8772147575a70426 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Fri, 11 Sep 2020 10:57:46 +0200
Subject: [PATCH 10/10] WIP: simplify reduce_combine_ranges

---
 src/backend/access/brin/brin_minmax_multi.c | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 7450bffc11..c8f36b0c8a 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1066,6 +1066,7 @@ reduce_combine_ranges(CombineRange *cranges, int ncranges,
 {
 	int i;
 	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
 
 	/*
 	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
@@ -1077,7 +1078,7 @@ reduce_combine_ranges(CombineRange *cranges, int ncranges,
 		int j;
 		int shortest;
 
-		if (count_values(cranges, ncranges) <= max_values * 0.75)
+		if (count <= max_values * 0.75)
 			break;
 
 		shortest = distances[i].index;
@@ -1089,6 +1090,11 @@ reduce_combine_ranges(CombineRange *cranges, int ncranges,
 		 */
 		Assert(shortest < (ncranges - 1));
 
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
 		/*
 		 * Move the values to join the two selected ranges. The new range is
 		 * definiely not collapsed but a regular range.
-- 
2.25.4

#92John Naylor
john.naylor@2ndquadrant.com
In reply to: Tomas Vondra (#91)
Re: WIP: BRIN multi-range indexes

On Fri, Sep 11, 2020 at 6:14 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

I understand. I just feel a bit uneasy about replacing an index with
something that may or may not be better for a certain use case. I mean,
if you have data set for which regular minmax works fine, wouldn't you
be annoyed if we just switched it for something slower?

How about making multi minmax the default for new indexes, and those
who know their data will stay very well correlated can specify simple
minmax ops for speed? Upgraded indexes would stay the same, and only
new ones would have the risk of slowdown if not attended to.

Also, I wonder if the slowdown in building a new index is similar to
the slowdown for updates. I'd like to run some TCP-H tests (that will
take some time).

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#93Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: John Naylor (#92)
6 attachment(s)
Re: WIP: BRIN multi-range indexes

On Fri, Sep 11, 2020 at 10:08:15AM -0400, John Naylor wrote:

On Fri, Sep 11, 2020 at 6:14 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

I understand. I just feel a bit uneasy about replacing an index with
something that may or may not be better for a certain use case. I mean,
if you have data set for which regular minmax works fine, wouldn't you
be annoyed if we just switched it for something slower?

How about making multi minmax the default for new indexes, and those
who know their data will stay very well correlated can specify simple
minmax ops for speed? Upgraded indexes would stay the same, and only
new ones would have the risk of slowdown if not attended to.

That might work, I think. I like that it's an explicit choice, i.e. we
may change what the default opclass is, but the behavior won't change
unexpectedly during REINDEX etc. It might still be a bit surprising
after dump/restore, but that's probably fine.

It would be ideal if the opclasses were binary compatible, allowing a
more seamless transition. Unfortunately that seems impossible, because
plain minmax uses two Datums to store the range, while multi-minmax uses
a more complex structure.

Also, I wonder if the slowdown in building a new index is similar to
the slowdown for updates. I'd like to run some TCP-H tests (that will
take some time).

It might be, because it needs to deserialize/serialize the summary too,
and there's no option to amortize the costs over many inserts. OTOH the
insert probably needs to do various other things, so maybe it's won't be
that bad. But yeah, testing and benchmarking it would be nice. Do you
plan to test just the minmax-multi opclass, or will you look at the
bloom one too?

Attached is a slightly improved version - I've merged the various pieces
into the "main" patches, and made some minor additional optimizations.
I've left the cost tweak as a separate part for now, though.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-keys-to-BRIN-consistent-function--20200911b.patchtext/plain; charset=us-asciiDownload
From 294da08cef9a67fcae2b305f0759161ba24465a0 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 13 Sep 2019 18:34:39 +0200
Subject: [PATCH 1/6] Pass all keys to BRIN consistent function at once

---
 src/backend/access/brin/brin.c           | 126 ++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 164 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 116 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 293 insertions(+), 121 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1f72562c60..6777d48faf 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,53 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +520,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,34 +530,19 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (!nkeys[attno - 1])
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
@@ -519,12 +554,43 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					 * the range as a whole, so break out of the loop as soon
 					 * as a false return value is obtained.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/* Check keys one by one */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..8968886ff5 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,103 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		matches;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = inclusion_consistent_key(bdesc, column, key, colloid);
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_BOOL(matches);
+}
+
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +376,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +437,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +458,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +477,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +512,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +522,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..1219a3a2ab 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,99 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
-	FmgrInfo   *finfo;
+	int			keyno;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		matches = minmax_consistent_key(bdesc, column, key, colloid);
+
+		/* found non-matching key */
+		if (!matches)
+			break;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(matches);
+}
+
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +283,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 687509ba92..925b262b60 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8104,7 +8104,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8120,7 +8120,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.25.4

0002-Move-IS-NOT-NULL-checks-to-bringetbitmap-20200911b.patchtext/plain; charset=us-asciiDownload
From 9728d0451731ee93e2eb9bb5982778e962253671 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 9 Jun 2019 22:05:39 +0200
Subject: [PATCH 2/6] Move IS NOT NULL checks to bringetbitmap

---
 src/backend/access/brin/brin.c           | 116 ++++++++++++++++++++---
 src/backend/access/brin/brin_inclusion.c |  62 +-----------
 src/backend/access/brin/brin_minmax.c    |  62 +-----------
 3 files changed, 109 insertions(+), 131 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 6777d48faf..caf7b62688 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,8 +389,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +417,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -440,14 +445,12 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 							  keyattno - 1)->attcollation));
 
 		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -455,9 +458,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -544,6 +561,83 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys, and
+					 * if we're violating them. In that case we can terminate
+					 * early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					{
+						ScanKey	key = nullkeys[attno - 1][keyno];
+
+						Assert(key->sk_attno == bval->bv_attno);
+
+						/* interrupt the loop as soon as we find a mismatch */
+						if (!addrange)
+							break;
+
+						/* handle IS NULL/IS NOT NULL tests */
+						if (key->sk_flags & SK_ISNULL)
+						{
+							/* IS NULL scan key, but range has no NULLs */
+							if (key->sk_flags & SK_SEARCHNULL)
+							{
+								if (!bval->bv_allnulls && !bval->bv_hasnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * For IS NOT NULL, we can only skip ranges that are
+							 * known to have only nulls.
+							 */
+							if (key->sk_flags & SK_SEARCHNOTNULL)
+							{
+								if (bval->bv_allnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * Neither IS NULL nor IS NOT NULL was used; assume all
+							 * indexable operators are strict and thus return false
+							 * with NULL value in the scan key.
+							 */
+							addrange = false;
+						}
+					}
+
+					/*
+					 * If any of the IS [NOT] NULL keys failed, the page range as
+					 * a whole can't pass. So terminate the loop.
+					 */
+					if (!addrange)
+						break;
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed. If
+					 * there are no regular scan keys, we're done - the page range
+					 * matches. If there are regular keys, but the page range is
+					 * marked as 'all nulls' it can't possibly pass (we're assuming
+					 * the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 8968886ff5..22edc6b46f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -265,63 +265,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
 	bool		matches;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -333,9 +276,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = inclusion_consistent_key(bdesc, column, key, colloid);
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 1219a3a2ab..7a7bd21cec 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -153,63 +153,6 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	Oid			colloid = PG_GET_COLLATION();
 	Datum		matches;
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	matches = true;
 
@@ -217,9 +160,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		matches = minmax_consistent_key(bdesc, column, key, colloid);
 
-- 
2.25.4

0003-Move-processing-of-NULLs-from-BRIN-support-20200911b.patchtext/plain; charset=us-asciiDownload
From 60a582baa498e7fe1befaa6a37063c5bccabc334 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 2 Apr 2020 02:56:00 +0200
Subject: [PATCH 3/6] Move processing of NULLs from BRIN support functions

---
 src/backend/access/brin/brin.c           | 260 ++++++++++++++---------
 src/backend/access/brin/brin_inclusion.c |  44 +---
 src/backend/access/brin/brin_minmax.c    |  41 +---
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 169 insertions(+), 179 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index caf7b62688..a9c44c0b82 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -359,6 +337,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -562,69 +541,31 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
-					 * First check if there are any IS [NOT] NULL scan keys, and
-					 * if we're violating them. In that case we can terminate
-					 * early, without invoking the support function.
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
 					 *
 					 * As there may be more keys, we can only detemine mismatch
 					 * within this loop.
 					 */
-					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
 					{
-						ScanKey	key = nullkeys[attno - 1][keyno];
-
-						Assert(key->sk_attno == bval->bv_attno);
-
-						/* interrupt the loop as soon as we find a mismatch */
-						if (!addrange)
-							break;
-
-						/* handle IS NULL/IS NOT NULL tests */
-						if (key->sk_flags & SK_ISNULL)
-						{
-							/* IS NULL scan key, but range has no NULLs */
-							if (key->sk_flags & SK_SEARCHNULL)
-							{
-								if (!bval->bv_allnulls && !bval->bv_hasnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * For IS NOT NULL, we can only skip ranges that are
-							 * known to have only nulls.
-							 */
-							if (key->sk_flags & SK_SEARCHNOTNULL)
-							{
-								if (bval->bv_allnulls)
-									addrange = false;
-
-								continue;
-							}
-
-							/*
-							 * Neither IS NULL nor IS NOT NULL was used; assume all
-							 * indexable operators are strict and thus return false
-							 * with NULL value in the scan key.
-							 */
-							addrange = false;
-						}
-					}
-
-					/*
-					 * If any of the IS [NOT] NULL keys failed, the page range as
-					 * a whole can't pass. So terminate the loop.
-					 */
-					if (!addrange)
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
 						break;
+					}
 
 					/*
-					 * So either there are no IS [NOT] NULL keys, or all passed. If
-					 * there are no regular scan keys, we're done - the page range
-					 * matches. If there are regular keys, but the page range is
-					 * marked as 'all nulls' it can't possibly pass (we're assuming
-					 * the operators are strict).
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
 					 */
 
 					/* No regular scan keys - page range as a whole passes. */
@@ -772,7 +713,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -801,25 +741,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1598,6 +1521,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1651,3 +1607,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 22edc6b46f..59503b6f68 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -510,37 +500,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 7a7bd21cec..8882eec12c 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -245,34 +235,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

0004-BRIN-bloom-indexes-20200911b.patchtext/plain; charset=us-asciiDownload
From c4a0815938be1fd5d4cabd85bfb363d254eb7b4e Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sat, 5 Sep 2020 23:54:39 +0200
Subject: [PATCH 4/6] BRIN bloom indexes

add special pg_brin_bloom_summary data type

do merge sort in bloom_compact
---
 doc/src/sgml/brin.sgml                    |  178 ++++
 doc/src/sgml/ref/create_index.sgml        |   31 +
 src/backend/access/brin/Makefile          |    1 +
 src/backend/access/brin/brin_bloom.c      | 1107 +++++++++++++++++++++
 src/include/access/brin.h                 |    2 +
 src/include/access/brin_internal.h        |    4 +
 src/include/catalog/pg_amop.dat           |  170 ++++
 src/include/catalog/pg_amproc.dat         |  447 +++++++++
 src/include/catalog/pg_opclass.dat        |   72 ++
 src/include/catalog/pg_opfamily.dat       |   38 +
 src/include/catalog/pg_proc.dat           |   34 +
 src/include/catalog/pg_type.dat           |    7 +
 src/test/regress/expected/brin_bloom.out  |  456 +++++++++
 src/test/regress/expected/opr_sanity.out  |    3 +-
 src/test/regress/expected/psql.out        |    3 +-
 src/test/regress/expected/type_sanity.out |    7 +-
 src/test/regress/parallel_schedule        |    5 +
 src/test/regress/serial_schedule          |    1 +
 src/test/regress/sql/brin_bloom.sql       |  404 ++++++++
 19 files changed, 2965 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 33aa64e81d..9c90d451a7 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -555,6 +555,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..c2cbbd9400
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,1107 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i, j, k;
+	Size	len;
+
+	uint32 *values;
+	uint32 *result;
+	uint32 *sorted;
+	uint32 *unsorted;
+
+	int		nvalues;
+	int		nsorted;
+	int		nunsorted;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* when already fully sorted, no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* result of merging */
+	k = 0;
+	result = (uint32 *) palloc(sizeof(uint32) * filter->nvalues);
+
+	/* first we have sorted data, then unsorted */
+	sorted = values;
+	nsorted = filter->nsorted;
+
+	unsorted = &values[filter->nsorted];
+	nunsorted = filter->nvalues - filter->nsorted;
+
+	/* sort the unsorted part, then merge the two parts */
+	pg_qsort(unsorted, nunsorted, sizeof(uint32), cmp_uint32);
+
+	i = 0;	/* sorted index */
+	j = 0;	/* unsorted index */
+
+	while ((i < nsorted) && (j < nunsorted))
+	{
+		if (sorted[i] <= unsorted[j])
+			result[k++] = sorted[i++];
+		else
+			result[k++] = unsorted[j++];
+	}
+
+	while (i < nsorted)
+		result[k++] = sorted[i++];
+
+	while (j < nunsorted)
+		result[k++] = unsorted[j++];
+
+	/* compact - remove duplicate values */
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (result[i] != result[nvalues - 1])
+			result[nvalues++] = result[i];
+	}
+
+	memcpy(filter->data, result, sizeof(uint32) * nvalues);
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BRINBLOOMSUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..12033d48ec 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1936,6 +1999,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2002,6 +2070,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2101,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2123,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2145,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2187,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2209,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2360,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2415,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2437,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2493,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2515,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2581,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 37b580883f..4e35bb3459 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -770,6 +770,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -783,6 +801,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -796,6 +832,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -897,6 +951,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -910,6 +1016,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -922,6 +1046,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -934,6 +1075,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -984,6 +1142,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -998,6 +1195,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1012,6 +1229,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1025,6 +1262,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1059,6 +1314,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1072,6 +1347,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1179,6 +1472,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1193,6 +1542,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1207,6 +1576,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1247,6 +1636,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1260,6 +1669,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1295,6 +1722,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..ca747a03b9 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +389,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..8875079698 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 925b262b60..dc17d4ce38 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8127,6 +8127,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -10973,3 +10993,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index b2cec07416..a41c2e5418 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -631,3 +631,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034', oid_symbol => 'BRINBLOOMSUMMARYOID',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 1b3c146e4c..dca8e9eb34 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2034,6 +2034,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2097,7 +2098,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index daac0ff49d..72c7598c89 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4989,8 +4989,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..b49239b1d0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..abce8e180a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -107,6 +107,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.25.4

0005-BRIN-minmax-multi-indexes-20200911b.patchtext/plain; charset=us-asciiDownload
From 534ae347fc015d6aa624315be8016f5416ebb2f8 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 6 Sep 2020 00:10:44 +0200
Subject: [PATCH 5/6] BRIN minmax-multi indexes

add special pg_brin_minmax_multi_summary data type

WIP: batch build

WIP: simplify reduce_combine_ranges
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2390 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    8 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4796 insertions(+), 22 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 9c90d451a7..f6b8fbd1d6 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -586,6 +586,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 64.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..227927fcf5
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2390 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BRINMINMAXMULTISUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
+
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 46e6b23c87..8bdf837070 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -138,6 +138,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		for (datumno = 0;
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
@@ -398,6 +406,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -477,6 +490,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index a9ccc3995b..064a93d09b 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 2f1f144db4..b0993f8ac5 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -165,14 +165,14 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  *		when the .dat files in src/include/catalog/ do not specify an OID
  *		for a catalog entry that requires one.
  *
- *		OIDS 12000-16383 are reserved for assignment during initdb
- *		using the OID generator.  (We start the generator at 12000.)
+ *		OIDS 13000-16383 are reserved for assignment during initdb
+ *		using the OID generator.  (We start the generator at 13000.)
  *
  *		OIDs beginning at 16384 are assigned from the OID generator
  *		during normal multiuser operation.  (We force the generator up to
  *		16384 as soon as we are in normal operation.)
  *
- * The choices of 8000, 10000 and 12000 are completely arbitrary, and can be
+ * The choices of 8000, 10000 and 13000 are completely arbitrary, and can be
  * moved if we run low on OIDs in any category.  Changing the macros below,
  * and updating relevant documentation (see bki.sgml and RELEASE_CHANGES),
  * should be sufficient to do this.  Moving the 16384 boundary between
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 12033d48ec..cda1ad4fbe 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1900,6 +1900,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1977,6 +2123,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2003,6 +2166,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2070,6 +2249,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2101,6 +2346,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2123,6 +2385,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2424,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2209,6 +2505,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2360,6 +2673,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2415,6 +2874,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2437,6 +2913,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2493,6 +2986,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2515,6 +3025,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2581,6 +3108,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4e35bb3459..082fbe10b5 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -951,6 +951,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1046,6 +1192,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1092,6 +1255,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1142,6 +1322,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1195,6 +1449,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1229,6 +1503,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1262,6 +1556,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1347,6 +1661,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1472,6 +1805,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1542,6 +2039,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1576,6 +2093,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1636,6 +2173,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1667,7 +2224,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1722,6 +2300,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index ca747a03b9..27ef11b81b 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,6 +307,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -306,33 +318,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -348,36 +378,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -389,6 +437,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -398,6 +449,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -407,6 +461,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 8875079698..8e6d9eec16 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,26 +220,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -242,10 +262,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -254,12 +278,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dc17d4ce38..77735c3abe 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8110,6 +8110,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11007,3 +11072,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index a41c2e5418..c189b35a3d 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -638,3 +638,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039', oid_symbol => 'BRINMINMAXMULTISUMMARYOID',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 72c7598c89..f855633634 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4986,12 +4986,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 97bf9797de..55a30a6b47 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b49239b1d0..a933db5456 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index abce8e180a..3f05a7061f 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.25.4

0006-tweak-costing-for-bloom-minmax-multi-index-20200911b.patchtext/plain; charset=us-asciiDownload
From 2da736b9b73d6c048e707c473070725bd642bb8d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Fri, 7 Aug 2020 15:53:55 +0200
Subject: [PATCH 6/6] tweak costing for bloom/minmax-multi indexes

---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index c2cbbd9400..03e9d4b713 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -681,6 +681,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 227927fcf5..c8f36b0c8a 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1306,6 +1306,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 00c7afc66f..c00265a66d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7350,7 +7351,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7372,6 +7374,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7405,6 +7408,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7475,6 +7489,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

#94John Naylor
john.naylor@2ndquadrant.com
In reply to: Tomas Vondra (#93)
Re: WIP: BRIN multi-range indexes

On Fri, Sep 11, 2020 at 2:05 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

that bad. But yeah, testing and benchmarking it would be nice. Do you
plan to test just the minmax-multi opclass, or will you look at the
bloom one too?

Yeah, I'll start looking at bloom next week, and I'll include it when
I do perf testing.

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#95Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: John Naylor (#94)
5 attachment(s)
Re: WIP: BRIN multi-range indexes

On Fri, Sep 11, 2020 at 03:19:58PM -0400, John Naylor wrote:

On Fri, Sep 11, 2020 at 2:05 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

that bad. But yeah, testing and benchmarking it would be nice. Do you
plan to test just the minmax-multi opclass, or will you look at the
bloom one too?

Yeah, I'll start looking at bloom next week, and I'll include it when
I do perf testing.

OK. Here is a slightly improved version of the patch series, with better
commit messages and comments, and with the two patches tweaking handling
of NULL values merged into one.

As mentioned in my reply to Alvaro, I'm hoping to get the first two
parts (general improvements) committed soon, so that we can focus on the
new opclasses. I now recall why I was postponing pushing those parts
because it's primarily "just" a preparation for the new opclasses. Both
the scan keys and NULL handling tweaks are not going to help existing
opclasses very much, I think.

The NULL-handling might help a bit, but the scan key changes are mostly
irrelevant. So I'm wondering if we should even change the two existing
opclasses, instead of keeping them as they are (the code actually
supports that by checking number of arguments of the constitent
function). Opinions?

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20200912.patchtext/plain; charset=us-asciiDownload
From 2085561ad36af6994a81c1c6001fbd636077ed38 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/5] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1f72562c60..ccf609e799 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +528,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,51 +538,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..853727190c 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +443,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..a3bb3558ec 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+ 	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 687509ba92..925b262b60 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8104,7 +8104,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8120,7 +8120,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.25.4

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20200912.patchtext/plain; charset=us-asciiDownload
From d7812522e57ebb89aed73af5f5d736e380a8ac1f Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:28 +0200
Subject: [PATCH 2/5] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit, and it simplifies the support
procedures quite a bit, as they don't need to care about NULL values and
flags at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 287 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +--------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 235 insertions(+), 264 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index ccf609e799..83c71d0f8e 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -359,6 +337,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -389,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +396,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,23 +423,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -463,9 +448,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -552,6 +551,45 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -694,7 +732,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -723,25 +760,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1520,6 +1540,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1573,3 +1626,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 853727190c..f5eaef416f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -574,37 +506,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index a3bb3558ec..a625ef7aac 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
  	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

0003-BRIN-bloom-indexes-20200912.patchtext/plain; charset=us-asciiDownload
From 2839692af136e25083b00cba8c98f42411b05098 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:43 +0200
Subject: [PATCH 3/5] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    |  178 ++++
 doc/src/sgml/ref/create_index.sgml        |   31 +
 src/backend/access/brin/Makefile          |    1 +
 src/backend/access/brin/brin_bloom.c      | 1107 +++++++++++++++++++++
 src/include/access/brin.h                 |    2 +
 src/include/access/brin_internal.h        |    4 +
 src/include/catalog/pg_amop.dat           |  170 ++++
 src/include/catalog/pg_amproc.dat         |  447 +++++++++
 src/include/catalog/pg_opclass.dat        |   72 ++
 src/include/catalog/pg_opfamily.dat       |   38 +
 src/include/catalog/pg_proc.dat           |   34 +
 src/include/catalog/pg_type.dat           |    7 +
 src/test/regress/expected/brin_bloom.out  |  456 +++++++++
 src/test/regress/expected/opr_sanity.out  |    3 +-
 src/test/regress/expected/psql.out        |    3 +-
 src/test/regress/expected/type_sanity.out |    7 +-
 src/test/regress/parallel_schedule        |    5 +
 src/test/regress/serial_schedule          |    1 +
 src/test/regress/sql/brin_bloom.sql       |  404 ++++++++
 19 files changed, 2965 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 33aa64e81d..9c90d451a7 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -555,6 +555,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..c2cbbd9400
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,1107 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i, j, k;
+	Size	len;
+
+	uint32 *values;
+	uint32 *result;
+	uint32 *sorted;
+	uint32 *unsorted;
+
+	int		nvalues;
+	int		nsorted;
+	int		nunsorted;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* when already fully sorted, no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* result of merging */
+	k = 0;
+	result = (uint32 *) palloc(sizeof(uint32) * filter->nvalues);
+
+	/* first we have sorted data, then unsorted */
+	sorted = values;
+	nsorted = filter->nsorted;
+
+	unsorted = &values[filter->nsorted];
+	nunsorted = filter->nvalues - filter->nsorted;
+
+	/* sort the unsorted part, then merge the two parts */
+	pg_qsort(unsorted, nunsorted, sizeof(uint32), cmp_uint32);
+
+	i = 0;	/* sorted index */
+	j = 0;	/* unsorted index */
+
+	while ((i < nsorted) && (j < nunsorted))
+	{
+		if (sorted[i] <= unsorted[j])
+			result[k++] = sorted[i++];
+		else
+			result[k++] = unsorted[j++];
+	}
+
+	while (i < nsorted)
+		result[k++] = sorted[i++];
+
+	while (j < nunsorted)
+		result[k++] = unsorted[j++];
+
+	/* compact - remove duplicate values */
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (result[i] != result[nvalues - 1])
+			result[nvalues++] = result[i];
+	}
+
+	memcpy(filter->data, result, sizeof(uint32) * nvalues);
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BRINBLOOMSUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..12033d48ec 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1936,6 +1999,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2002,6 +2070,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2101,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2123,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2145,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2187,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2209,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2360,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2415,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2437,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2493,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2515,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2581,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 37b580883f..4e35bb3459 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -770,6 +770,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -783,6 +801,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -796,6 +832,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -897,6 +951,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -910,6 +1016,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -922,6 +1046,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -934,6 +1075,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -984,6 +1142,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -998,6 +1195,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1012,6 +1229,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1025,6 +1262,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1059,6 +1314,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1072,6 +1347,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1179,6 +1472,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1193,6 +1542,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1207,6 +1576,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1247,6 +1636,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1260,6 +1669,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1295,6 +1722,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..ca747a03b9 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +389,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..8875079698 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 925b262b60..dc17d4ce38 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8127,6 +8127,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -10973,3 +10993,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index b2cec07416..a41c2e5418 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -631,3 +631,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034', oid_symbol => 'BRINBLOOMSUMMARYOID',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 1b3c146e4c..dca8e9eb34 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2034,6 +2034,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2097,7 +2098,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index daac0ff49d..72c7598c89 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4989,8 +4989,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..b49239b1d0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..abce8e180a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -107,6 +107,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.25.4

0004-BRIN-minmax-multi-indexes-20200912.patchtext/plain; charset=us-asciiDownload
From 87f5f02c4c0df16715e7ca5064f22bc6c1e64f8c Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:49 +0200
Subject: [PATCH 4/5] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2390 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4793 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 9c90d451a7..d86e3ed8f2 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -586,6 +586,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..227927fcf5
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2390 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BRINMINMAXMULTISUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
+
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 46e6b23c87..8bdf837070 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -138,6 +138,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		for (datumno = 0;
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
@@ -398,6 +406,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -477,6 +490,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index a9ccc3995b..064a93d09b 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 2f1f144db4..d3d12a7b99 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 12033d48ec..cda1ad4fbe 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1900,6 +1900,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1977,6 +2123,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2003,6 +2166,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2070,6 +2249,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2101,6 +2346,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2123,6 +2385,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2424,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2209,6 +2505,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2360,6 +2673,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2415,6 +2874,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2437,6 +2913,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2493,6 +2986,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2515,6 +3025,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2581,6 +3108,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4e35bb3459..082fbe10b5 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -951,6 +951,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1046,6 +1192,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1092,6 +1255,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1142,6 +1322,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1195,6 +1449,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1229,6 +1503,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1262,6 +1556,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1347,6 +1661,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1472,6 +1805,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1542,6 +2039,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1576,6 +2093,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1636,6 +2173,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1667,7 +2224,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1722,6 +2300,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index ca747a03b9..27ef11b81b 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,6 +307,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -306,33 +318,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -348,36 +378,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -389,6 +437,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -398,6 +449,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -407,6 +461,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 8875079698..8e6d9eec16 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,26 +220,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -242,10 +262,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -254,12 +278,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dc17d4ce38..77735c3abe 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8110,6 +8110,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11007,3 +11072,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index a41c2e5418..c189b35a3d 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -638,3 +638,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039', oid_symbol => 'BRINMINMAXMULTISUMMARYOID',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 72c7598c89..f855633634 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4986,12 +4986,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 97bf9797de..55a30a6b47 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b49239b1d0..a933db5456 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index abce8e180a..3f05a7061f 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.25.4

0005-Ignore-correlation-for-new-BRIN-opclasses-20200912.patchtext/plain; charset=us-asciiDownload
From 38f2d03d252595fe007b65fc097cc688b3909931 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 5/5] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index c2cbbd9400..03e9d4b713 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -681,6 +681,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 227927fcf5..c8f36b0c8a 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1306,6 +1306,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 00c7afc66f..c00265a66d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7350,7 +7351,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7372,6 +7374,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7405,6 +7408,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7475,6 +7489,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

#96Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#95)
6 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

while running some benchmarks to see if the first two patches cause any
regressions, I found a bug in 0002 which reworks the NULL handling. The
code failed to eliminate ranges early using the IS NULL scan keys,
resulting in expensive recheck. The attached version fixes that.

I also noticed that some of the queries seem to be slightly slower, most
likely due to bringetbitmap having to split the scan keys per attribute,
which also requires some allocations etc. The regression is fairly small
might be just noise (less than 2-3% in most cases), but it seems just
allocating everything in a single chunk eliminates most of it - this is
what the new 0003 patch does.

OTOH the rework also helps in other cases - I've measured ~2-3% speedups
for cases where moving the IS NULL handling to bringetbitmap eliminates
calls to the consistent function (e.g. IS NULL queries on columns with
no NULL values).

These results seems very dependent on the hardware (especially CPU),
though, and the differences are pretty small in general (1-2%).

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0004-BRIN-bloom-indexes-20200913.patchtext/plain; charset=us-asciiDownload
From 2c6933aebef102df53c145a6ae6c794e7860883f Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:43 +0200
Subject: [PATCH 4/6] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    |  178 ++++
 doc/src/sgml/ref/create_index.sgml        |   31 +
 src/backend/access/brin/Makefile          |    1 +
 src/backend/access/brin/brin_bloom.c      | 1107 +++++++++++++++++++++
 src/include/access/brin.h                 |    2 +
 src/include/access/brin_internal.h        |    4 +
 src/include/catalog/pg_amop.dat           |  170 ++++
 src/include/catalog/pg_amproc.dat         |  447 +++++++++
 src/include/catalog/pg_opclass.dat        |   72 ++
 src/include/catalog/pg_opfamily.dat       |   38 +
 src/include/catalog/pg_proc.dat           |   34 +
 src/include/catalog/pg_type.dat           |    7 +
 src/test/regress/expected/brin_bloom.out  |  456 +++++++++
 src/test/regress/expected/opr_sanity.out  |    3 +-
 src/test/regress/expected/psql.out        |    3 +-
 src/test/regress/expected/type_sanity.out |    7 +-
 src/test/regress/parallel_schedule        |    5 +
 src/test/regress/serial_schedule          |    1 +
 src/test/regress/sql/brin_bloom.sql       |  404 ++++++++
 19 files changed, 2965 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 33aa64e81d..9c90d451a7 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -555,6 +555,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..c2cbbd9400
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,1107 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i, j, k;
+	Size	len;
+
+	uint32 *values;
+	uint32 *result;
+	uint32 *sorted;
+	uint32 *unsorted;
+
+	int		nvalues;
+	int		nsorted;
+	int		nunsorted;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* when already fully sorted, no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* result of merging */
+	k = 0;
+	result = (uint32 *) palloc(sizeof(uint32) * filter->nvalues);
+
+	/* first we have sorted data, then unsorted */
+	sorted = values;
+	nsorted = filter->nsorted;
+
+	unsorted = &values[filter->nsorted];
+	nunsorted = filter->nvalues - filter->nsorted;
+
+	/* sort the unsorted part, then merge the two parts */
+	pg_qsort(unsorted, nunsorted, sizeof(uint32), cmp_uint32);
+
+	i = 0;	/* sorted index */
+	j = 0;	/* unsorted index */
+
+	while ((i < nsorted) && (j < nunsorted))
+	{
+		if (sorted[i] <= unsorted[j])
+			result[k++] = sorted[i++];
+		else
+			result[k++] = unsorted[j++];
+	}
+
+	while (i < nsorted)
+		result[k++] = sorted[i++];
+
+	while (j < nunsorted)
+		result[k++] = unsorted[j++];
+
+	/* compact - remove duplicate values */
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (result[i] != result[nvalues - 1])
+			result[nvalues++] = result[i];
+	}
+
+	memcpy(filter->data, result, sizeof(uint32) * nvalues);
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BRINBLOOMSUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..12033d48ec 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1936,6 +1999,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2002,6 +2070,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2101,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2123,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2145,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2187,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2209,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2360,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2415,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2437,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2493,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2515,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2581,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 37b580883f..4e35bb3459 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -770,6 +770,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -783,6 +801,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -796,6 +832,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -897,6 +951,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -910,6 +1016,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -922,6 +1046,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -934,6 +1075,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -984,6 +1142,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -998,6 +1195,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1012,6 +1229,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1025,6 +1262,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1059,6 +1314,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1072,6 +1347,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1179,6 +1472,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1193,6 +1542,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1207,6 +1576,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1247,6 +1636,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1260,6 +1669,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1295,6 +1722,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..ca747a03b9 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +389,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..8875079698 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 925b262b60..dc17d4ce38 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8127,6 +8127,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -10973,3 +10993,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index b2cec07416..a41c2e5418 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -631,3 +631,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034', oid_symbol => 'BRINBLOOMSUMMARYOID',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 1b3c146e4c..dca8e9eb34 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2034,6 +2034,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2097,7 +2098,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index daac0ff49d..72c7598c89 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4989,8 +4989,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..b49239b1d0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..abce8e180a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -107,6 +107,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.25.4

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20200913.patchtext/plain; charset=us-asciiDownload
From 5106be5ed355cdaca5becf7c215df95bb8dd45ea Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:28 +0200
Subject: [PATCH 2/6] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit, and it simplifies the support
procedures quite a bit, as they don't need to care about NULL values and
flags at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 294 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 240 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index ccf609e799..f438e59c75 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -359,6 +337,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -389,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +396,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,23 +423,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -463,9 +448,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,15 +542,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -694,7 +735,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -723,25 +763,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1520,6 +1543,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1573,3 +1629,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 853727190c..f5eaef416f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -574,37 +506,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index a3bb3558ec..a625ef7aac 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
  	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

0003-optimize-allocations-20200913.patchtext/plain; charset=us-asciiDownload
From e79f8905ecb5271e82d560a0ce2e5ab4342eb2b4 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/6] optimize allocations

---
 src/backend/access/brin/brin.c | 52 +++++++++++++++++++++++++++-------
 1 file changed, 42 insertions(+), 10 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index f438e59c75..756dc0fd1f 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,8 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -398,11 +400,47 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -451,17 +489,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.25.4

0004-BRIN-bloom-indexes-20200913.patchtext/plain; charset=us-asciiDownload
From 2c6933aebef102df53c145a6ae6c794e7860883f Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:43 +0200
Subject: [PATCH 4/6] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    |  178 ++++
 doc/src/sgml/ref/create_index.sgml        |   31 +
 src/backend/access/brin/Makefile          |    1 +
 src/backend/access/brin/brin_bloom.c      | 1107 +++++++++++++++++++++
 src/include/access/brin.h                 |    2 +
 src/include/access/brin_internal.h        |    4 +
 src/include/catalog/pg_amop.dat           |  170 ++++
 src/include/catalog/pg_amproc.dat         |  447 +++++++++
 src/include/catalog/pg_opclass.dat        |   72 ++
 src/include/catalog/pg_opfamily.dat       |   38 +
 src/include/catalog/pg_proc.dat           |   34 +
 src/include/catalog/pg_type.dat           |    7 +
 src/test/regress/expected/brin_bloom.out  |  456 +++++++++
 src/test/regress/expected/opr_sanity.out  |    3 +-
 src/test/regress/expected/psql.out        |    3 +-
 src/test/regress/expected/type_sanity.out |    7 +-
 src/test/regress/parallel_schedule        |    5 +
 src/test/regress/serial_schedule          |    1 +
 src/test/regress/sql/brin_bloom.sql       |  404 ++++++++
 19 files changed, 2965 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 33aa64e81d..9c90d451a7 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -555,6 +555,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..c2cbbd9400
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,1107 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i, j, k;
+	Size	len;
+
+	uint32 *values;
+	uint32 *result;
+	uint32 *sorted;
+	uint32 *unsorted;
+
+	int		nvalues;
+	int		nsorted;
+	int		nunsorted;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* when already fully sorted, no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* result of merging */
+	k = 0;
+	result = (uint32 *) palloc(sizeof(uint32) * filter->nvalues);
+
+	/* first we have sorted data, then unsorted */
+	sorted = values;
+	nsorted = filter->nsorted;
+
+	unsorted = &values[filter->nsorted];
+	nunsorted = filter->nvalues - filter->nsorted;
+
+	/* sort the unsorted part, then merge the two parts */
+	pg_qsort(unsorted, nunsorted, sizeof(uint32), cmp_uint32);
+
+	i = 0;	/* sorted index */
+	j = 0;	/* unsorted index */
+
+	while ((i < nsorted) && (j < nunsorted))
+	{
+		if (sorted[i] <= unsorted[j])
+			result[k++] = sorted[i++];
+		else
+			result[k++] = unsorted[j++];
+	}
+
+	while (i < nsorted)
+		result[k++] = sorted[i++];
+
+	while (j < nunsorted)
+		result[k++] = unsorted[j++];
+
+	/* compact - remove duplicate values */
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (result[i] != result[nvalues - 1])
+			result[nvalues++] = result[i];
+	}
+
+	memcpy(filter->data, result, sizeof(uint32) * nvalues);
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BRINBLOOMSUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..12033d48ec 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1936,6 +1999,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2002,6 +2070,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2101,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2123,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2145,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2187,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2209,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2360,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2415,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2437,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2493,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2515,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2581,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 37b580883f..4e35bb3459 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -770,6 +770,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -783,6 +801,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -796,6 +832,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -897,6 +951,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -910,6 +1016,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -922,6 +1046,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -934,6 +1075,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -984,6 +1142,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -998,6 +1195,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1012,6 +1229,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1025,6 +1262,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1059,6 +1314,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1072,6 +1347,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1179,6 +1472,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1193,6 +1542,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1207,6 +1576,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1247,6 +1636,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1260,6 +1669,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1295,6 +1722,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..ca747a03b9 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +389,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..8875079698 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 925b262b60..dc17d4ce38 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8127,6 +8127,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -10973,3 +10993,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index b2cec07416..a41c2e5418 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -631,3 +631,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034', oid_symbol => 'BRINBLOOMSUMMARYOID',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 1b3c146e4c..dca8e9eb34 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2034,6 +2034,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2097,7 +2098,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index daac0ff49d..72c7598c89 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4989,8 +4989,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..b49239b1d0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..abce8e180a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -107,6 +107,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.25.4

0005-BRIN-minmax-multi-indexes-20200913.patchtext/plain; charset=us-asciiDownload
From 26b756c0e84c745b1da8d3fe71bb583c6e68fc67 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:49 +0200
Subject: [PATCH 5/6] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2390 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4793 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 9c90d451a7..d86e3ed8f2 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -586,6 +586,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..227927fcf5
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2390 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BRINMINMAXMULTISUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
+
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 46e6b23c87..8bdf837070 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -138,6 +138,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		for (datumno = 0;
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
@@ -398,6 +406,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -477,6 +490,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index a9ccc3995b..064a93d09b 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 2f1f144db4..d3d12a7b99 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 12033d48ec..cda1ad4fbe 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1900,6 +1900,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1977,6 +2123,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2003,6 +2166,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2070,6 +2249,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2101,6 +2346,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2123,6 +2385,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2424,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2209,6 +2505,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2360,6 +2673,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2415,6 +2874,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2437,6 +2913,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2493,6 +2986,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2515,6 +3025,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2581,6 +3108,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4e35bb3459..082fbe10b5 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -951,6 +951,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1046,6 +1192,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1092,6 +1255,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1142,6 +1322,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1195,6 +1449,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1229,6 +1503,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1262,6 +1556,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1347,6 +1661,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1472,6 +1805,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1542,6 +2039,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1576,6 +2093,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1636,6 +2173,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1667,7 +2224,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1722,6 +2300,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index ca747a03b9..27ef11b81b 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,6 +307,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -306,33 +318,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -348,36 +378,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -389,6 +437,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -398,6 +449,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -407,6 +461,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 8875079698..8e6d9eec16 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,26 +220,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -242,10 +262,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -254,12 +278,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dc17d4ce38..77735c3abe 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8110,6 +8110,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11007,3 +11072,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index a41c2e5418..c189b35a3d 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -638,3 +638,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039', oid_symbol => 'BRINMINMAXMULTISUMMARYOID',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 72c7598c89..f855633634 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4986,12 +4986,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 97bf9797de..55a30a6b47 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b49239b1d0..a933db5456 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index abce8e180a..3f05a7061f 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.25.4

0006-Ignore-correlation-for-new-BRIN-opclasses-20200913.patchtext/plain; charset=us-asciiDownload
From 738fdb48b797d87125a6fea58f52c539b0feaa1d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 6/6] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index c2cbbd9400..03e9d4b713 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -681,6 +681,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 227927fcf5..c8f36b0c8a 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1306,6 +1306,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 00c7afc66f..c00265a66d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7350,7 +7351,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7372,6 +7374,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7405,6 +7408,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7475,6 +7489,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

#97John Naylor
john.naylor@2ndquadrant.com
In reply to: Tomas Vondra (#96)
Re: WIP: BRIN multi-range indexes

On Sun, Sep 13, 2020 at 12:40 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

<20200913 patch set>

Hi Tomas,

The cfbot fails to apply this, but with 0001 from 0912 it works on my
end, so going with that.

One problem I have is I don't get success with the new reloptions:

create index cd_multi on iot using brin(create_dt
timestamptz_minmax_multi_ops) with (values_per_range = 64);
ERROR: unrecognized parameter "values_per_range"

create index on iot using brin(create_dt timestamptz_bloom_ops) with
(n_distinct_per_range = 16);
ERROR: unrecognized parameter "n_distinct_per_range"

Aside from that, I'm going to try to understand the code, and ask
questions. Some of the code might still change, but I don't think it's
too early to do some comment and docs proofreading. I'll do this in
separate emails for bloom and multi-minmax to keep it from being too
long. Perf testing will come sometime later.

Bloom
-----

+ greater than 0.0 and smaller than 1.0. The default values is 0.01,

+ rows per block). The default values is <literal>-0.1</literal>, and

s/values/value/

+ the minimum number of distinct non-null values is <literal>128</literal>.

I don't see 128 in the code, but I do see this, is this the intention?:

#define BLOOM_MIN_NDISTINCT_PER_RANGE 16

+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.

s/test/testing/
s/wheter it containst/whether it contains/

+ * The index only supports equality operator, similarly to hash indexes.

s/operator/operators/

+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.

Sounds like the first sentence should start with "although".

+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead

I think you accidentally a word.

+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).

and

+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But

Maybe I'm missing something, but the first two comments don't match
the last one -- I don't see where we get table ndistinct, which I take
to mean from the stats catalog?

+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.

IIUC, it's after reaching a certain size (BLOOM_MAX_UNSORTED * 4), so
"same" doesn't make sense here.

+ /*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define BLOOM_DEFAULT_FALSE_POSITIVE_RATE 0.01 /* 1% fp rate */

I think we'd want better stated justification for this default, even
if just precedence in other implementations. Maybe I can test some
other values here?

+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.

Yeah, I think it's obvious enough to leave out.

+ m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 /
(pow(2.0, log(2.0)))));

I find this pretty hard to read and pgindent is going to mess it up
further. I would have a comment with the formula in math notation
(note that you can dispense with the reciprocal and just use
negation), but in code fold the last part to a constant. That might go
against project style, though:

m = ceil(ndistinct * log(false_positive_rate) * -2.08136);

+ * XXX Maybe the 64B min size is not really needed?

Something to figure out before commit?

+ /* assume 'not updated' by default */
+ Assert(filter);

I don't see how these are related.

+ big_h = ((uint32) DatumGetInt64(hash_uint32(value)));

I'm curious about the Int64 part -- most callers use the bare value or
with DatumGetUInt32().

Also, is there a reference for the algorithm for hash values that
follows? I didn't see anything like it in my cursory reading of the
topic. Might be good to include it in the comments.

+ * Tweak the ndistinct value based on the pagesPerRange value. First,

Nitpick: "Tweak" to my mind means to adjust an existing value. The
above is only true if ndistinct is negative, but we're really not
tweaking, but using it as a scale factor. Otherwise it's not adjusted,
only clamped.

+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?

I don't understand this.

+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+ return BloomGetFalsePositiveRate(opts);
+}

The body of the function is just a macro not used anywhere else -- is
there a reason for having the macro? Also, what's the first parameter
for?

Similarly, BloomGetNDistinctPerRange could just be inlined or turned
into a function for readability.

+ * or null if it is not exists.

s/is not exists/does not exist/

+ /*
+ * XXX not sure the detoasting is necessary (probably not, this
+ * can only be in an index).
+ */

Something to find out before commit?

+ /* TODO include the sorted/unsorted values */

Patch TODO or future TODO?

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#98Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: John Naylor (#97)
6 attachment(s)
Re: WIP: BRIN multi-range indexes

On Thu, Sep 17, 2020 at 10:33:06AM -0400, John Naylor wrote:

On Sun, Sep 13, 2020 at 12:40 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

<20200913 patch set>

Hi Tomas,

The cfbot fails to apply this, but with 0001 from 0912 it works on my
end, so going with that.

Hmm, it seems to fail because of some whitespace errors. Attached is an
updated version resolving that.

One problem I have is I don't get success with the new reloptions:

create index cd_multi on iot using brin(create_dt
timestamptz_minmax_multi_ops) with (values_per_range = 64);
ERROR: unrecognized parameter "values_per_range"

create index on iot using brin(create_dt timestamptz_bloom_ops) with
(n_distinct_per_range = 16);
ERROR: unrecognized parameter "n_distinct_per_range"

But those are opclass parameters, so the parameters are not specified in
WITH clause, but right after the opclass name:

CREATE INDEX idx ON table USING brin (
bigint_col int8_minmax_multi_ops(values_per_range = 15)
);

Aside from that, I'm going to try to understand the code, and ask
questions. Some of the code might still change, but I don't think it's
too early to do some comment and docs proofreading. I'll do this in
separate emails for bloom and multi-minmax to keep it from being too
long. Perf testing will come sometime later.

OK.

Bloom
-----

+ greater than 0.0 and smaller than 1.0. The default values is 0.01,

+ rows per block). The default values is <literal>-0.1</literal>, and

s/values/value/

+ the minimum number of distinct non-null values is <literal>128</literal>.

I don't see 128 in the code, but I do see this, is this the intention?:

#define BLOOM_MIN_NDISTINCT_PER_RANGE 16

Ah, that's right - I might have lowered the default after writing the
comment. Will fix.

+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.

s/test/testing/
s/wheter it containst/whether it contains/

OK, will reword.

+ * The index only supports equality operator, similarly to hash indexes.

s/operator/operators/

Hmmm, are there really multiple equality operators?

+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.

Sounds like the first sentence should start with "although".

Yeah, probably. Or maybe there should be "but" at the beginning of the
second sentence.

+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead

I think you accidentally a word.

Seems like that.

+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).

and

+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But

Maybe I'm missing something, but the first two comments don't match
the last one -- I don't see where we get table ndistinct, which I take
to mean from the stats catalog?

Ah, right. The part suggesting we're looking at the table n_distinct
estimate is obsolete - some older version of the patch attempted to do
that, but I decided to remove it at some point. We can add it in the
future, but I'll fix the comment for now.

+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.

IIUC, it's after reaching a certain size (BLOOM_MAX_UNSORTED * 4), so
"same" doesn't make sense here.

Ummm, no. BLOOM_MAX_UNSORTED has nothing to do with the switch from
sorted mode to hashing (which is storing an actual bloom filter).

BLOOM_MAX_UNSORTED only determines number of new items that may not
be sorted - we don't sort after each insertion, but only once in a while
to amortize the costs. So for example you may have 1000 sorted values
and then we allow adding 32 new ones before sorting the array again
(using a merge sort). But all of this is in the "sorted" mode.

The number of items the comment refers to is this:

/* how many uint32 hashes can we fit into the bitmap */
int maxvalues = filter->nbits / (8 * sizeof(uint32));

where nbits is the size of the bloom filter. So I think the "same" is
quite right here.

+ /*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define BLOOM_DEFAULT_FALSE_POSITIVE_RATE 0.01 /* 1% fp rate */

I think we'd want better stated justification for this default, even
if just precedence in other implementations. Maybe I can test some
other values here?

Well, I don't know how to pick a better default :-( Ultimately it's a
tarde-off between larger indexes and scanning larger fraction of a table
due to lower false positive. Then there's the restriction that the whole
index tuple needs to fit into a single 8kB page.

+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.

Yeah, I think it's obvious enough to leave out.

+ m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 /
(pow(2.0, log(2.0)))));

I find this pretty hard to read and pgindent is going to mess it up
further. I would have a comment with the formula in math notation
(note that you can dispense with the reciprocal and just use
negation), but in code fold the last part to a constant. That might go
against project style, though:

m = ceil(ndistinct * log(false_positive_rate) * -2.08136);

Hmm, maybe. I've mostly just copied this out from some bloom filter
paper, but maybe it's not readable.

+ * XXX Maybe the 64B min size is not really needed?

Something to figure out before commit?

Probably. I think this optimization is somewhat pointless and we should
just allocate the right amount of space, and repalloc if needed.

+ /* assume 'not updated' by default */
+ Assert(filter);

I think they are not related, although the formatting might make it seem
like that.

I don't see how these are related.

+ big_h = ((uint32) DatumGetInt64(hash_uint32(value)));

I'm curious about the Int64 part -- most callers use the bare value or
with DatumGetUInt32().

Yeah, that formula should use DatumGetUInt32.

Also, is there a reference for the algorithm for hash values that
follows? I didn't see anything like it in my cursory reading of the
topic. Might be good to include it in the comments.

This was suggested by Yura Sokolov [1]/messages/by-id/94c173b54a0aef6ae9b18157ef52f03e@postgrespro.ru in 2017. The post refers to a
paper [2]https://www.eecs.harvard.edu/~michaelm/postscripts/rsa2008.pdf but I don't recall which part describes "our" algorithm.

[1]: /messages/by-id/94c173b54a0aef6ae9b18157ef52f03e@postgrespro.ru
[2]: https://www.eecs.harvard.edu/~michaelm/postscripts/rsa2008.pdf

+ * Tweak the ndistinct value based on the pagesPerRange value. First,

Nitpick: "Tweak" to my mind means to adjust an existing value. The
above is only true if ndistinct is negative, but we're really not
tweaking, but using it as a scale factor. Otherwise it's not adjusted,
only clamped.

OK. Perhaps 'adjust' would be a better term?

+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?

I don't understand this.

IIRC I thought there were situations when pagesPerRange value is not
defined, e.g. in read-only access. But I'm not sure about this, and
cosidering the code actally does not check for that (in fact, it has an
assert enforcing valid block number) I think it's a stale comment.

+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+ return BloomGetFalsePositiveRate(opts);
+}

The body of the function is just a macro not used anywhere else -- is
there a reason for having the macro? Also, what's the first parameter
for?

No reason. I think the function used to be more complicated at some
point, but it got simpler.

Similarly, BloomGetNDistinctPerRange could just be inlined or turned
into a function for readability.

Same story.

+ * or null if it is not exists.

s/is not exists/does not exist/

+ /*
+ * XXX not sure the detoasting is necessary (probably not, this
+ * can only be in an index).
+ */

Something to find out before commit?

+ /* TODO include the sorted/unsorted values */

This was simplemented as part of the discussion about pageinspect, and
I wanted some confirmation if the approach is acceptable or not before
spending more time on it. Also, the values are really just hashes of the
column values, so I'm not quite sure it makes sense to include that.
What would you suggest?

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20200917.patchtext/plain; charset=us-asciiDownload
From 83608b94f83080c5c46eeda684ed14a2baee2005 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/6] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1f72562c60..ccf609e799 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +528,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,51 +538,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..853727190c 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +443,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..b233558310 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 96d7efd427..2d013d4221 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8107,7 +8107,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8123,7 +8123,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.25.4

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20200917.patchtext/plain; charset=us-asciiDownload
From 3c271506ab49e04d9eb3528aea21c72efcb7f730 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/6] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit, and it simplifies the support
procedures quite a bit, as they don't need to care about NULL values and
flags at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 294 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 240 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index ccf609e799..f438e59c75 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -359,6 +337,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -389,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +396,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,23 +423,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -463,9 +448,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,15 +542,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -694,7 +735,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -723,25 +763,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1520,6 +1543,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1573,3 +1629,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 853727190c..f5eaef416f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -574,37 +506,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index b233558310..8886ad984e 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

0003-optimize-allocations-20200917.patchtext/plain; charset=us-asciiDownload
From 506f74d961b1bb3e413f8cfce05d5ef183202866 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/6] optimize allocations

---
 src/backend/access/brin/brin.c | 52 +++++++++++++++++++++++++++-------
 1 file changed, 42 insertions(+), 10 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index f438e59c75..756dc0fd1f 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,8 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -398,11 +400,47 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -451,17 +489,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.25.4

0004-BRIN-bloom-indexes-20200917.patchtext/plain; charset=us-asciiDownload
From a7fd79ab6eb40ffd982efa063edc2738bc6d9d5f Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:43 +0200
Subject: [PATCH 4/6] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    |  178 ++++
 doc/src/sgml/ref/create_index.sgml        |   31 +
 src/backend/access/brin/Makefile          |    1 +
 src/backend/access/brin/brin_bloom.c      | 1107 +++++++++++++++++++++
 src/include/access/brin.h                 |    2 +
 src/include/access/brin_internal.h        |    4 +
 src/include/catalog/pg_amop.dat           |  170 ++++
 src/include/catalog/pg_amproc.dat         |  447 +++++++++
 src/include/catalog/pg_opclass.dat        |   72 ++
 src/include/catalog/pg_opfamily.dat       |   38 +
 src/include/catalog/pg_proc.dat           |   34 +
 src/include/catalog/pg_type.dat           |    7 +
 src/test/regress/expected/brin_bloom.out  |  456 +++++++++
 src/test/regress/expected/opr_sanity.out  |    3 +-
 src/test/regress/expected/psql.out        |    3 +-
 src/test/regress/expected/type_sanity.out |    7 +-
 src/test/regress/parallel_schedule        |    5 +
 src/test/regress/serial_schedule          |    1 +
 src/test/regress/sql/brin_bloom.sql       |  404 ++++++++
 19 files changed, 2965 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 33aa64e81d..9c90d451a7 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -555,6 +555,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..c2cbbd9400
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,1107 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i, j, k;
+	Size	len;
+
+	uint32 *values;
+	uint32 *result;
+	uint32 *sorted;
+	uint32 *unsorted;
+
+	int		nvalues;
+	int		nsorted;
+	int		nunsorted;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* when already fully sorted, no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* result of merging */
+	k = 0;
+	result = (uint32 *) palloc(sizeof(uint32) * filter->nvalues);
+
+	/* first we have sorted data, then unsorted */
+	sorted = values;
+	nsorted = filter->nsorted;
+
+	unsorted = &values[filter->nsorted];
+	nunsorted = filter->nvalues - filter->nsorted;
+
+	/* sort the unsorted part, then merge the two parts */
+	pg_qsort(unsorted, nunsorted, sizeof(uint32), cmp_uint32);
+
+	i = 0;	/* sorted index */
+	j = 0;	/* unsorted index */
+
+	while ((i < nsorted) && (j < nunsorted))
+	{
+		if (sorted[i] <= unsorted[j])
+			result[k++] = sorted[i++];
+		else
+			result[k++] = unsorted[j++];
+	}
+
+	while (i < nsorted)
+		result[k++] = sorted[i++];
+
+	while (j < nunsorted)
+		result[k++] = unsorted[j++];
+
+	/* compact - remove duplicate values */
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (result[i] != result[nvalues - 1])
+			result[nvalues++] = result[i];
+	}
+
+	memcpy(filter->data, result, sizeof(uint32) * nvalues);
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BRINBLOOMSUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..12033d48ec 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1936,6 +1999,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2002,6 +2070,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2101,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2123,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2145,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2187,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2209,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2360,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2415,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2437,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2493,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2515,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2581,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index a8e0c4ff8a..15f3baea15 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -772,6 +772,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -785,6 +803,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -798,6 +834,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -899,6 +953,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -912,6 +1018,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -924,6 +1048,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -936,6 +1077,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -986,6 +1144,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1000,6 +1197,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1014,6 +1231,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1027,6 +1264,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1061,6 +1316,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1074,6 +1349,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1181,6 +1474,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1195,6 +1544,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1209,6 +1578,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1249,6 +1638,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1262,6 +1671,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1297,6 +1724,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..ca747a03b9 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +389,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..8875079698 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2d013d4221..321ab372f4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8130,6 +8130,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -10976,3 +10996,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index b2cec07416..a41c2e5418 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -631,3 +631,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034', oid_symbol => 'BRINBLOOMSUMMARYOID',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 1b3c146e4c..dca8e9eb34 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2034,6 +2034,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2097,7 +2098,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index daac0ff49d..72c7598c89 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4989,8 +4989,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..b49239b1d0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..abce8e180a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -107,6 +107,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.25.4

0005-BRIN-minmax-multi-indexes-20200917.patchtext/plain; charset=us-asciiDownload
From 95d9628fcef75323294e3f6f8ad3d3e52f3c764a Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:49 +0200
Subject: [PATCH 5/6] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2389 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4792 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 9c90d451a7..d86e3ed8f2 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -586,6 +586,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..62643e49d4
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2389 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BRINMINMAXMULTISUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 46e6b23c87..8bdf837070 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -138,6 +138,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		for (datumno = 0;
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
@@ -398,6 +406,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -477,6 +490,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index a9ccc3995b..064a93d09b 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 2f1f144db4..d3d12a7b99 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 12033d48ec..cda1ad4fbe 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1900,6 +1900,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1977,6 +2123,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2003,6 +2166,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2070,6 +2249,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2101,6 +2346,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2123,6 +2385,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2424,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2209,6 +2505,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2360,6 +2673,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2415,6 +2874,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2437,6 +2913,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2493,6 +2986,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2515,6 +3025,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2581,6 +3108,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 15f3baea15..8d212ede8b 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -953,6 +953,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1048,6 +1194,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1094,6 +1257,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1144,6 +1324,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1197,6 +1451,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1231,6 +1505,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1264,6 +1558,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1349,6 +1663,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1474,6 +1807,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1544,6 +2041,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1578,6 +2095,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1638,6 +2175,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1669,7 +2226,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1724,6 +2302,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index ca747a03b9..27ef11b81b 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,6 +307,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -306,33 +318,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -348,36 +378,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -389,6 +437,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -398,6 +449,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -407,6 +461,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 8875079698..8e6d9eec16 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,26 +220,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -242,10 +262,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -254,12 +278,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 321ab372f4..206edf0963 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8113,6 +8113,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11010,3 +11075,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index a41c2e5418..c189b35a3d 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -638,3 +638,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039', oid_symbol => 'BRINMINMAXMULTISUMMARYOID',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 72c7598c89..f855633634 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4986,12 +4986,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 97bf9797de..55a30a6b47 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b49239b1d0..a933db5456 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index abce8e180a..3f05a7061f 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.25.4

0006-Ignore-correlation-for-new-BRIN-opclasses-20200917.patchtext/plain; charset=us-asciiDownload
From b997e7746ffdad3fc23f9941655b38c15b76c6d7 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 6/6] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index c2cbbd9400..03e9d4b713 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -681,6 +681,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 62643e49d4..6a90b1610f 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1306,6 +1306,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 00c7afc66f..c00265a66d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7350,7 +7351,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7372,6 +7374,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7405,6 +7408,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7475,6 +7489,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

#99Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#98)
6 attachment(s)
Re: WIP: BRIN multi-range indexes

OK,

cfbot was not quite happy with the last version either - there was a bug
in 0003 part, allocating smaller chunk of memory than needed. Attached
is a version fixing that, hopefully cfbot will be happy with this one.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-scan-keys-to-BRIN-consistent-func-20200917b.patchtext/plain; charset=us-asciiDownload
From 83608b94f83080c5c46eeda684ed14a2baee2005 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/6] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1f72562c60..ccf609e799 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +528,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,51 +538,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..853727190c 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +443,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..b233558310 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 96d7efd427..2d013d4221 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8107,7 +8107,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8123,7 +8123,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.25.4

0002-Move-IS-NOT-NULL-handling-from-BRIN-suppor-20200917b.patchtext/plain; charset=us-asciiDownload
From 3c271506ab49e04d9eb3528aea21c72efcb7f730 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/6] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit, and it simplifies the support
procedures quite a bit, as they don't need to care about NULL values and
flags at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 294 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 240 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index ccf609e799..f438e59c75 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -359,6 +337,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -389,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +396,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,23 +423,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -463,9 +448,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,15 +542,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -694,7 +735,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -723,25 +763,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1520,6 +1543,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1573,3 +1629,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 853727190c..f5eaef416f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -574,37 +506,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index b233558310..8886ad984e 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

0003-optimize-allocations-20200917b.patchtext/plain; charset=us-asciiDownload
From aaa523a489ec02e1b2ac00f7cfd866859e4e4b94 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/6] optimize allocations

---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index f438e59c75..bd052c8b0b 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -398,11 +401,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -438,9 +480,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -451,17 +493,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.25.4

0004-BRIN-bloom-indexes-20200917b.patchtext/plain; charset=us-asciiDownload
From bcb684685028d00173fed5c703afdecc79f7f9a2 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:43 +0200
Subject: [PATCH 4/6] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    |  178 ++++
 doc/src/sgml/ref/create_index.sgml        |   31 +
 src/backend/access/brin/Makefile          |    1 +
 src/backend/access/brin/brin_bloom.c      | 1107 +++++++++++++++++++++
 src/include/access/brin.h                 |    2 +
 src/include/access/brin_internal.h        |    4 +
 src/include/catalog/pg_amop.dat           |  170 ++++
 src/include/catalog/pg_amproc.dat         |  447 +++++++++
 src/include/catalog/pg_opclass.dat        |   72 ++
 src/include/catalog/pg_opfamily.dat       |   38 +
 src/include/catalog/pg_proc.dat           |   34 +
 src/include/catalog/pg_type.dat           |    7 +
 src/test/regress/expected/brin_bloom.out  |  456 +++++++++
 src/test/regress/expected/opr_sanity.out  |    3 +-
 src/test/regress/expected/psql.out        |    3 +-
 src/test/regress/expected/type_sanity.out |    7 +-
 src/test/regress/parallel_schedule        |    5 +
 src/test/regress/serial_schedule          |    1 +
 src/test/regress/sql/brin_bloom.sql       |  404 ++++++++
 19 files changed, 2965 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 33aa64e81d..9c90d451a7 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -555,6 +555,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..c2cbbd9400
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,1107 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i, j, k;
+	Size	len;
+
+	uint32 *values;
+	uint32 *result;
+	uint32 *sorted;
+	uint32 *unsorted;
+
+	int		nvalues;
+	int		nsorted;
+	int		nunsorted;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* when already fully sorted, no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* result of merging */
+	k = 0;
+	result = (uint32 *) palloc(sizeof(uint32) * filter->nvalues);
+
+	/* first we have sorted data, then unsorted */
+	sorted = values;
+	nsorted = filter->nsorted;
+
+	unsorted = &values[filter->nsorted];
+	nunsorted = filter->nvalues - filter->nsorted;
+
+	/* sort the unsorted part, then merge the two parts */
+	pg_qsort(unsorted, nunsorted, sizeof(uint32), cmp_uint32);
+
+	i = 0;	/* sorted index */
+	j = 0;	/* unsorted index */
+
+	while ((i < nsorted) && (j < nunsorted))
+	{
+		if (sorted[i] <= unsorted[j])
+			result[k++] = sorted[i++];
+		else
+			result[k++] = unsorted[j++];
+	}
+
+	while (i < nsorted)
+		result[k++] = sorted[i++];
+
+	while (j < nunsorted)
+		result[k++] = unsorted[j++];
+
+	/* compact - remove duplicate values */
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (result[i] != result[nvalues - 1])
+			result[nvalues++] = result[i];
+	}
+
+	memcpy(filter->data, result, sizeof(uint32) * nvalues);
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BRINBLOOMSUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..12033d48ec 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1936,6 +1999,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2002,6 +2070,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2101,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2123,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2145,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2187,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2209,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2360,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2415,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2437,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2493,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2515,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2581,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index a8e0c4ff8a..15f3baea15 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -772,6 +772,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -785,6 +803,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -798,6 +834,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -899,6 +953,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -912,6 +1018,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -924,6 +1048,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -936,6 +1077,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -986,6 +1144,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1000,6 +1197,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1014,6 +1231,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1027,6 +1264,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1061,6 +1316,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1074,6 +1349,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1181,6 +1474,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1195,6 +1544,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1209,6 +1578,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1249,6 +1638,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1262,6 +1671,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1297,6 +1724,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..ca747a03b9 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +389,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..8875079698 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2d013d4221..321ab372f4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8130,6 +8130,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -10976,3 +10996,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index b2cec07416..a41c2e5418 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -631,3 +631,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034', oid_symbol => 'BRINBLOOMSUMMARYOID',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 1b3c146e4c..dca8e9eb34 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2034,6 +2034,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2097,7 +2098,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index daac0ff49d..72c7598c89 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4989,8 +4989,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..b49239b1d0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..abce8e180a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -107,6 +107,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.25.4

0005-BRIN-minmax-multi-indexes-20200917b.patchtext/plain; charset=us-asciiDownload
From 84dc19e447d353f6fb52ae8eeb5fc694740f1e81 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:49 +0200
Subject: [PATCH 5/6] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2389 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4792 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 9c90d451a7..d86e3ed8f2 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -586,6 +586,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..62643e49d4
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2389 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BRINMINMAXMULTISUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 46e6b23c87..8bdf837070 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -138,6 +138,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		for (datumno = 0;
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
@@ -398,6 +406,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -477,6 +490,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index a9ccc3995b..064a93d09b 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 2f1f144db4..d3d12a7b99 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 12033d48ec..cda1ad4fbe 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1900,6 +1900,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1977,6 +2123,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2003,6 +2166,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2070,6 +2249,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2101,6 +2346,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2123,6 +2385,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2424,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2209,6 +2505,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2360,6 +2673,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2415,6 +2874,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2437,6 +2913,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2493,6 +2986,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2515,6 +3025,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2581,6 +3108,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 15f3baea15..8d212ede8b 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -953,6 +953,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1048,6 +1194,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1094,6 +1257,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1144,6 +1324,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1197,6 +1451,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1231,6 +1505,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1264,6 +1558,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1349,6 +1663,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1474,6 +1807,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1544,6 +2041,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1578,6 +2095,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1638,6 +2175,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1669,7 +2226,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1724,6 +2302,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index ca747a03b9..27ef11b81b 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,6 +307,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -306,33 +318,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -348,36 +378,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -389,6 +437,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -398,6 +449,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -407,6 +461,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 8875079698..8e6d9eec16 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,26 +220,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -242,10 +262,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -254,12 +278,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 321ab372f4..206edf0963 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8113,6 +8113,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11010,3 +11075,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index a41c2e5418..c189b35a3d 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -638,3 +638,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039', oid_symbol => 'BRINMINMAXMULTISUMMARYOID',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 72c7598c89..f855633634 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4986,12 +4986,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 97bf9797de..55a30a6b47 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b49239b1d0..a933db5456 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index abce8e180a..3f05a7061f 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.25.4

0006-Ignore-correlation-for-new-BRIN-opclasses-20200917b.patchtext/plain; charset=us-asciiDownload
From 00a87aa8eff3c2ab046f3319470ebfbe38f56fc1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 6/6] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index c2cbbd9400..03e9d4b713 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -681,6 +681,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 62643e49d4..6a90b1610f 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1306,6 +1306,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 00c7afc66f..c00265a66d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7350,7 +7351,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7372,6 +7374,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7405,6 +7408,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7475,6 +7489,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.25.4

#100John Naylor
john.naylor@2ndquadrant.com
In reply to: Tomas Vondra (#98)
Re: WIP: BRIN multi-range indexes

On Thu, Sep 17, 2020 at 12:34 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Thu, Sep 17, 2020 at 10:33:06AM -0400, John Naylor wrote:

On Sun, Sep 13, 2020 at 12:40 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

<20200913 patch set>

But those are opclass parameters, so the parameters are not specified in
WITH clause, but right after the opclass name:

CREATE INDEX idx ON table USING brin (
bigint_col int8_minmax_multi_ops(values_per_range = 15)
);

D'oh!

+ * The index only supports equality operator, similarly to hash indexes.

s/operator/operators/

Hmmm, are there really multiple equality operators?

Ah, I see what you meant -- then "_the_ equality operator" is what we want.

The number of items the comment refers to is this:

/* how many uint32 hashes can we fit into the bitmap */
int maxvalues = filter->nbits / (8 * sizeof(uint32));

where nbits is the size of the bloom filter. So I think the "same" is
quite right here.

Ok, I get it now.

+ /*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define BLOOM_DEFAULT_FALSE_POSITIVE_RATE 0.01 /* 1% fp rate */

I think we'd want better stated justification for this default, even
if just precedence in other implementations. Maybe I can test some
other values here?

Well, I don't know how to pick a better default :-( Ultimately it's a
tarde-off between larger indexes and scanning larger fraction of a table
due to lower false positive. Then there's the restriction that the whole
index tuple needs to fit into a single 8kB page.

Well, it might be a perfectly good default, and it seems common in
articles on the topic, but the comment is referring to aesthetics. :-)
I still intend to test some cases.

Also, is there a reference for the algorithm for hash values that
follows? I didn't see anything like it in my cursory reading of the
topic. Might be good to include it in the comments.

This was suggested by Yura Sokolov [1] in 2017. The post refers to a
paper [2] but I don't recall which part describes "our" algorithm.

[1] /messages/by-id/94c173b54a0aef6ae9b18157ef52f03e@postgrespro.ru
[2] https://www.eecs.harvard.edu/~michaelm/postscripts/rsa2008.pdf

Hmm, I came across that paper while doing background reading. Okay,
now I get that "% (filter->nbits - 1)" is the second hash function in
that scheme. But now I wonder if that second function should actually
act on the passed "value" (the original hash), so that they are
actually independent, as required. In the language of that paper, the
patch seems to have

g(x) = h1(x) + i*h2(h1(x)) + f(i)

instead of

g(x) = h1(x) + i*h2(x) + f(i)

Concretely, I'm wondering if it should be:

 big_h = DatumGetUint32(hash_uint32(value));
 h = big_h % filter->nbits;
-d = big_h % (filter->nbits - 1);
+d = value % (filter->nbits - 1);

But I could be wrong.

Also, I take it that f(i) = 1 in the patch, hence the increment here:

+ h += d++;

But it's a little hidden. That might be worth commenting, if I haven't
completely missed something.

+ * Tweak the ndistinct value based on the pagesPerRange value. First,

Nitpick: "Tweak" to my mind means to adjust an existing value. The
above is only true if ndistinct is negative, but we're really not
tweaking, but using it as a scale factor. Otherwise it's not adjusted,
only clamped.

OK. Perhaps 'adjust' would be a better term?

I felt like rewriting the whole thing, but your original gets the
point across ok, really.

"If the passed ndistinct value is positive, we can just use that, but
we also clamp the value to prevent over-sizing the bloom filter
unnecessarily. If it's negative, it represents a multiplier to apply
to the maximum number of tuples in the range (assuming each page gets
MaxHeapTuplesPerPage tuples, which is likely a significant
over-estimate), similar to the corresponding value in table
statistics."

+ /* TODO include the sorted/unsorted values */

This was simplemented as part of the discussion about pageinspect, and
I wanted some confirmation if the approach is acceptable or not before
spending more time on it. Also, the values are really just hashes of the
column values, so I'm not quite sure it makes sense to include that.
What would you suggest?

My gut feeling is the hashes are not useful for this purpose, but I
don't feel strongly either way.

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#101John Naylor
john.naylor@2ndquadrant.com
In reply to: John Naylor (#100)
Re: WIP: BRIN multi-range indexes

I wrote:

Hmm, I came across that paper while doing background reading. Okay,
now I get that "% (filter->nbits - 1)" is the second hash function in
that scheme. But now I wonder if that second function should actually
act on the passed "value" (the original hash), so that they are
actually independent, as required. In the language of that paper, the
patch seems to have

g(x) = h1(x) + i*h2(h1(x)) + f(i)

instead of

g(x) = h1(x) + i*h2(x) + f(i)

Concretely, I'm wondering if it should be:

big_h = DatumGetUint32(hash_uint32(value));
h = big_h % filter->nbits;
-d = big_h % (filter->nbits - 1);
+d = value % (filter->nbits - 1);

But I could be wrong.

I'm wrong -- if we use different operands to the moduli, we throw away
the assumption of co-primeness. But I'm still left wondering why we
have to re-hash the hash for this to work. In any case, there should
be some more documentation around the core algorithm, so that future
readers are not left scratching their heads.

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#102Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: John Naylor (#100)
Re: WIP: BRIN multi-range indexes

On Thu, Sep 17, 2020 at 05:42:59PM -0400, John Naylor wrote:

On Thu, Sep 17, 2020 at 12:34 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Thu, Sep 17, 2020 at 10:33:06AM -0400, John Naylor wrote:

On Sun, Sep 13, 2020 at 12:40 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

<20200913 patch set>

But those are opclass parameters, so the parameters are not specified in
WITH clause, but right after the opclass name:

CREATE INDEX idx ON table USING brin (
bigint_col int8_minmax_multi_ops(values_per_range = 15)
);

D'oh!

+ * The index only supports equality operator, similarly to hash indexes.

s/operator/operators/

Hmmm, are there really multiple equality operators?

Ah, I see what you meant -- then "_the_ equality operator" is what we want.

The number of items the comment refers to is this:

/* how many uint32 hashes can we fit into the bitmap */
int maxvalues = filter->nbits / (8 * sizeof(uint32));

where nbits is the size of the bloom filter. So I think the "same" is
quite right here.

Ok, I get it now.

+ /*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define BLOOM_DEFAULT_FALSE_POSITIVE_RATE 0.01 /* 1% fp rate */

I think we'd want better stated justification for this default, even
if just precedence in other implementations. Maybe I can test some
other values here?

Well, I don't know how to pick a better default :-( Ultimately it's a
tarde-off between larger indexes and scanning larger fraction of a table
due to lower false positive. Then there's the restriction that the whole
index tuple needs to fit into a single 8kB page.

Well, it might be a perfectly good default, and it seems common in
articles on the topic, but the comment is referring to aesthetics. :-)
I still intend to test some cases.

I think we may formulate this as a question of how much I/O we need to
do for a random query, and pick the false positive rate minimizing that.
For a single BRIN range an approximation might look like this:

bloom_size(fpr, ...) + (fpr * range_size) + (selectivity * range_size)

The "selectivity" shows the true selectivity of ranges, and it might be
esimated from a per-row selectivity I guess. But it does not matter much
because this is constant and independent of the false-positive rate, so
we can ignore it. Which leaves us with

bloom_size(fpr, ...) + (fpr * range_size)

We might solve this for fixed parameters (range_size, ndistinct, ...),
either analytically or by brute force, giving us the "optimal" fpr.

The trouble is the bloom_size is restricted, and we don't really know
the limit - the whole index tuple needs to fit on a single 8kB page, and
there may be other BRIN summaries etc. So I've opted to use a somewhat
defensive default for the false positive rate.

Also, is there a reference for the algorithm for hash values that
follows? I didn't see anything like it in my cursory reading of the
topic. Might be good to include it in the comments.

This was suggested by Yura Sokolov [1] in 2017. The post refers to a
paper [2] but I don't recall which part describes "our" algorithm.

[1] /messages/by-id/94c173b54a0aef6ae9b18157ef52f03e@postgrespro.ru
[2] https://www.eecs.harvard.edu/~michaelm/postscripts/rsa2008.pdf

Hmm, I came across that paper while doing background reading. Okay,
now I get that "% (filter->nbits - 1)" is the second hash function in
that scheme. But now I wonder if that second function should actually
act on the passed "value" (the original hash), so that they are
actually independent, as required. In the language of that paper, the
patch seems to have

g(x) = h1(x) + i*h2(h1(x)) + f(i)

instead of

g(x) = h1(x) + i*h2(x) + f(i)

Concretely, I'm wondering if it should be:

big_h = DatumGetUint32(hash_uint32(value));
h = big_h % filter->nbits;
-d = big_h % (filter->nbits - 1);
+d = value % (filter->nbits - 1);

But I could be wrong.

Also, I take it that f(i) = 1 in the patch, hence the increment here:

+ h += d++;

But it's a little hidden. That might be worth commenting, if I haven't
completely missed something.

OK

+ * Tweak the ndistinct value based on the pagesPerRange value. First,

Nitpick: "Tweak" to my mind means to adjust an existing value. The
above is only true if ndistinct is negative, but we're really not
tweaking, but using it as a scale factor. Otherwise it's not adjusted,
only clamped.

OK. Perhaps 'adjust' would be a better term?

I felt like rewriting the whole thing, but your original gets the
point across ok, really.

"If the passed ndistinct value is positive, we can just use that, but
we also clamp the value to prevent over-sizing the bloom filter
unnecessarily. If it's negative, it represents a multiplier to apply
to the maximum number of tuples in the range (assuming each page gets
MaxHeapTuplesPerPage tuples, which is likely a significant
over-estimate), similar to the corresponding value in table
statistics."

+ /* TODO include the sorted/unsorted values */

This was simplemented as part of the discussion about pageinspect, and
I wanted some confirmation if the approach is acceptable or not before
spending more time on it. Also, the values are really just hashes of the
column values, so I'm not quite sure it makes sense to include that.
What would you suggest?

My gut feeling is the hashes are not useful for this purpose, but I
don't feel strongly either way.

OK. I share this feeling.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#103Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: John Naylor (#101)
Re: WIP: BRIN multi-range indexes

On Thu, Sep 17, 2020 at 06:48:11PM -0400, John Naylor wrote:

I wrote:

Hmm, I came across that paper while doing background reading. Okay,
now I get that "% (filter->nbits - 1)" is the second hash function in
that scheme. But now I wonder if that second function should actually
act on the passed "value" (the original hash), so that they are
actually independent, as required. In the language of that paper, the
patch seems to have

g(x) = h1(x) + i*h2(h1(x)) + f(i)

instead of

g(x) = h1(x) + i*h2(x) + f(i)

Concretely, I'm wondering if it should be:

big_h = DatumGetUint32(hash_uint32(value));
h = big_h % filter->nbits;
-d = big_h % (filter->nbits - 1);
+d = value % (filter->nbits - 1);

But I could be wrong.

I'm wrong -- if we use different operands to the moduli, we throw away
the assumption of co-primeness. But I'm still left wondering why we
have to re-hash the hash for this to work. In any case, there should
be some more documentation around the core algorithm, so that future
readers are not left scratching their heads.

Hmm, good question. I think we don't really need to hash it twice. It
does not rally achieve anything - it won't reduce number of collisions
or anything like that.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#104John Naylor
john.naylor@2ndquadrant.com
In reply to: Tomas Vondra (#103)
Re: WIP: BRIN multi-range indexes

On Fri, Sep 18, 2020 at 7:40 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Thu, Sep 17, 2020 at 06:48:11PM -0400, John Naylor wrote:

I wrote:

Hmm, I came across that paper while doing background reading. Okay,
now I get that "% (filter->nbits - 1)" is the second hash function in
that scheme. But now I wonder if that second function should actually
act on the passed "value" (the original hash), so that they are
actually independent, as required. In the language of that paper, the
patch seems to have

g(x) = h1(x) + i*h2(h1(x)) + f(i)

instead of

g(x) = h1(x) + i*h2(x) + f(i)

Concretely, I'm wondering if it should be:

big_h = DatumGetUint32(hash_uint32(value));
h = big_h % filter->nbits;
-d = big_h % (filter->nbits - 1);
+d = value % (filter->nbits - 1);

But I could be wrong.

I'm wrong -- if we use different operands to the moduli, we throw away
the assumption of co-primeness. But I'm still left wondering why we
have to re-hash the hash for this to work. In any case, there should
be some more documentation around the core algorithm, so that future
readers are not left scratching their heads.

Hmm, good question. I think we don't really need to hash it twice. It
does not rally achieve anything - it won't reduce number of collisions
or anything like that.

Yeah, looking back at the discussion you linked previously, I think
it's a holdover from when the uint32 was rehashed with k different
seeds. Anyway, after thinking about it some more, I still have doubts
about the mapping algorithm. There are two stages to a hash mapping --
hashing and modulus. I don't think a single hash function (whether
rehashed or not) can be turned into two independent functions via a
choice of second modulus. At least, that's not what the Kirsch &
Mitzenmacher paper is claiming. Since we're not actually applying two
independent hash functions on the scan key, we're kind of shooting in
the dark.

It turns out there is something called a one-hash bloom filter, and
the paper in [1]https://www.researchgate.net/publication/284283336_One-Hashing_Bloom_Filter has a straightforward algorithm. Since we can
implement it exactly as stated in the paper, that gives me more
confidence in the real-world false positive rate. It goes like this:

Partition the filter bitmap into k partitions of similar but unequal
length, corresponding to consecutive prime numbers. Use the primes for
moduli of the uint32 value and map it to the bit of the corresponding
partition. For a simple example, let's use 7, 11, 13 for partitions in
a filter of size 31. The three bits are:

value % 7
7 + (value % 11)
7 + 11 + (value % 13)

We could store a const array of the first 256 primes. The largest such
prime is 1613, so with k=7 we can support up to ~11k bits, which is
more than we'd like to store anyway. Then we store the array index of
the largest prime in the 8bits of padding we currently have in
BloomFilter struct.

One wrinkle is that the sum of k primes is not going to match m
exactly. If the sum is too small, we can trim a few bits off of the
filter bitmap. If the sum is too large, the last partition can spill
into the front of the first one. This shouldn't matter much in the
common case since we need to round m to the nearest byte anyway.

This should be pretty straightforward to turn into code and I can take
a stab at it. Thoughts?

[1]: https://www.researchgate.net/publication/284283336_One-Hashing_Bloom_Filter

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#105Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: John Naylor (#104)
Re: WIP: BRIN multi-range indexes

On Fri, Sep 18, 2020 at 05:06:49PM -0400, John Naylor wrote:

On Fri, Sep 18, 2020 at 7:40 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Thu, Sep 17, 2020 at 06:48:11PM -0400, John Naylor wrote:

I wrote:

Hmm, I came across that paper while doing background reading. Okay,
now I get that "% (filter->nbits - 1)" is the second hash function in
that scheme. But now I wonder if that second function should actually
act on the passed "value" (the original hash), so that they are
actually independent, as required. In the language of that paper, the
patch seems to have

g(x) = h1(x) + i*h2(h1(x)) + f(i)

instead of

g(x) = h1(x) + i*h2(x) + f(i)

Concretely, I'm wondering if it should be:

big_h = DatumGetUint32(hash_uint32(value));
h = big_h % filter->nbits;
-d = big_h % (filter->nbits - 1);
+d = value % (filter->nbits - 1);

But I could be wrong.

I'm wrong -- if we use different operands to the moduli, we throw away
the assumption of co-primeness. But I'm still left wondering why we
have to re-hash the hash for this to work. In any case, there should
be some more documentation around the core algorithm, so that future
readers are not left scratching their heads.

Hmm, good question. I think we don't really need to hash it twice. It
does not rally achieve anything - it won't reduce number of collisions
or anything like that.

Yeah, looking back at the discussion you linked previously, I think
it's a holdover from when the uint32 was rehashed with k different
seeds. Anyway, after thinking about it some more, I still have doubts
about the mapping algorithm. There are two stages to a hash mapping --
hashing and modulus. I don't think a single hash function (whether
rehashed or not) can be turned into two independent functions via a
choice of second modulus. At least, that's not what the Kirsch &
Mitzenmacher paper is claiming. Since we're not actually applying two
independent hash functions on the scan key, we're kind of shooting in
the dark.

OK. I admit the modulo by nbits and (nbits - 1) is a bit suspicious, so
you may be right this is not quite correct construction.

The current scheme was meant to reduce the number of expensive hashing
calls (especially for low fpr values we may require quite a few of
those, easily 10 or more.

But maybe we could still use this scheme by actually computing

h1 = hash_uint32_extended(value, seed1);
h2 = hash_uint32_extended(value, seed2);

and then use this as the independent hash functions. I think that would
meet the requirements of the paper.

It turns out there is something called a one-hash bloom filter, and
the paper in [1] has a straightforward algorithm. Since we can
implement it exactly as stated in the paper, that gives me more
confidence in the real-world false positive rate. It goes like this:

Partition the filter bitmap into k partitions of similar but unequal
length, corresponding to consecutive prime numbers. Use the primes for
moduli of the uint32 value and map it to the bit of the corresponding
partition. For a simple example, let's use 7, 11, 13 for partitions in
a filter of size 31. The three bits are:

value % 7
7 + (value % 11)
7 + 11 + (value % 13)

We could store a const array of the first 256 primes. The largest such
prime is 1613, so with k=7 we can support up to ~11k bits, which is
more than we'd like to store anyway. Then we store the array index of
the largest prime in the 8bits of padding we currently have in
BloomFilter struct.

Why would 11k bits be more than we'd like to store? Assuming we could
use the whole 8kB page for the bloom filter, that'd be about 64k bits.
In practice there'd be a bit of overhead (page header ...) but it's
still much more than 11k bits. But I guess we can simply make the table
of primes a bit larger, right?

FWIW I don't think we need to be that careful about the space to store
stuff in padding etc. If we can - great, but compared to the size of the
filter it's negligible and I'd prioritize simplicity over a byte or two.

One wrinkle is that the sum of k primes is not going to match m
exactly. If the sum is too small, we can trim a few bits off of the
filter bitmap. If the sum is too large, the last partition can spill
into the front of the first one. This shouldn't matter much in the
common case since we need to round m to the nearest byte anyway.

AFAIK the paper simply says that as long as the sum of partitions is
close to the requested nbits, it's good enough. So I guess we could just
roll with that, no need to trim/wrap or something like that.

This should be pretty straightforward to turn into code and I can take
a stab at it. Thoughts?

Sure, go ahead. I'm happy someone is actually looking at those patches
and proposing alternative solutions, and this might easily be a better
hashing scheme.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#106John Naylor
john.naylor@2ndquadrant.com
In reply to: Tomas Vondra (#105)
Re: WIP: BRIN multi-range indexes

On Fri, Sep 18, 2020 at 6:27 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

But maybe we could still use this scheme by actually computing

h1 = hash_uint32_extended(value, seed1);
h2 = hash_uint32_extended(value, seed2);

and then use this as the independent hash functions. I think that would
meet the requirements of the paper.

Yeah, that would work algorithmically. It would be trivial to add to
the patch, too of course. There'd be a higher up-front cpu cost. Also,
I'm a bit cautious of rehashing hashes, and whether the two values
above are independent enough. I'm not sure either of these points
matters. My guess is the partition approach is more sound, but it has
some minor organizational challenges (see below).

Why would 11k bits be more than we'd like to store? Assuming we could
use the whole 8kB page for the bloom filter, that'd be about 64k bits.
In practice there'd be a bit of overhead (page header ...) but it's
still much more than 11k bits.

Brain fade -- I guess I thought we were avoiding being toasted, but
now I see that's not possible for BRIN storage. So, we'll want to
guard against this:

ERROR: index row size 8160 exceeds maximum 8152 for index "bar_num_idx"

While playing around with the numbers I had an epiphany: At the
defaults, the filter already takes up ~4.3kB, over half the page.
There is no room for another tuple, so if we're only indexing one
column, we might as well take up the whole page. Here MT = max tuples
per 128 8k pages, or 37120, so default ndistinct is 3712.

n k m p
MT/10 7 35580 0.01
MT/10 7 64000 0.0005
MT/10 12 64000 0.00025

Assuming ndistinct isn't way off reality, we get 20x-40x lower false
positive rate almost for free, and it'd be trivial to code! Keeping k
at 7 would be more robust, since it's equivalent to starting with n =
~6000, p = 0.006, which is still almost 2x less false positives than
you asked for. It also means nearly doubling the number of sorted
values before switching.

Going the other direction, capping nbits to 64k bits when ndistinct
gets too high, the false positive rate we can actually support starts
to drop. Here, the user requested 0.001 fpr.

n k p
4500 9 0.001
6000 7 0.006
7500 6 0.017
15000 3 0.129 (probably useless by now)
MT 1 0.440
64000 1 0.63 (possible with > 128 pages per range)

I imagine smaller pages_per_range settings are going to be useful for
skinny tables (note to self -- test). Maybe we could provide a way for
the user to see that their combination of pages_per_range, false
positive rate, and ndistinct is supportable, like
brin_bloom_get_supported_fpr(). Or document to check with
page_inspect. And that's not even considering multi-column indexes,
like you mentioned.

But I guess we can simply make the table
of primes a bit larger, right?

If we want to support all the above cases without falling down
entirely, it would have to go up to 32k to be safe (When k = 1 we
could degenerate to one modulus on the whole filter). That would be a
table of about 7kB, which we could binary search. [thinks for a
moment]...Actually, if we wanted to be clever, maybe we could
precalculate the primes needed for the 64k bit cases and stick them at
the end of the array. The usual algorithm will find them. That way, we
could keep the array around 2kB. However, for >8kB block size, we
couldn't adjust the 64k number, which might be okay, but not really
project policy.

We could also generate the primes via a sieve instead, which is really
fast (and more code). That would be more robust, but that would
require the filter to store the actual primes used, so 20 more bytes
at max k = 10. We could hard-code space for that, or to keep from
hard-coding maximum k and thus lowest possible false positive rate,
we'd need more bookkeeping.

So, with the partition approach, we'd likely have to set in stone
either max nbits, or min target false positive rate. The latter option
seems more principled, not only for the block size, but also since the
target fp rate is already fixed by the reloption, and as I've
demonstrated, we can often go above and beyond the reloption even
without changing k.

One wrinkle is that the sum of k primes is not going to match m
exactly. If the sum is too small, we can trim a few bits off of the
filter bitmap. If the sum is too large, the last partition can spill
into the front of the first one. This shouldn't matter much in the
common case since we need to round m to the nearest byte anyway.

AFAIK the paper simply says that as long as the sum of partitions is
close to the requested nbits, it's good enough. So I guess we could just
roll with that, no need to trim/wrap or something like that.

Hmm, I'm not sure I understand you. I can see not caring to trim
wasted bits, but we can't set/read off the end of the filter. If we
don't wrap, we could just skip reading/writing that bit. So a tiny
portion of access would be at k - 1. The paper is not clear on what to
do here, but they are explicit in minimizing the absolute value, which
could go on either side.

Also I found a bug:

+ add_local_real_reloption(relopts, "false_positive_rate",
+ "desired false-positive rate for the bloom filters",
+ BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+ 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));

When I set fp to 1.0, the reloption code is okay with that, but then
later the assertion gets triggered.

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#107Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: John Naylor (#106)
Re: WIP: BRIN multi-range indexes

On Mon, Sep 21, 2020 at 01:42:34PM -0400, John Naylor wrote:

On Fri, Sep 18, 2020 at 6:27 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

But maybe we could still use this scheme by actually computing

h1 = hash_uint32_extended(value, seed1);
h2 = hash_uint32_extended(value, seed2);

and then use this as the independent hash functions. I think that would
meet the requirements of the paper.

Yeah, that would work algorithmically. It would be trivial to add to
the patch, too of course. There'd be a higher up-front cpu cost. Also,
I'm a bit cautious of rehashing hashes, and whether the two values
above are independent enough. I'm not sure either of these points
matters. My guess is the partition approach is more sound, but it has
some minor organizational challenges (see below).

OK. I don't think rehashing hashes is an issue as long as the original
hash has sufficiently low collision rate (and while we know it's not
perfect we know it works well enough for hash indexes etc.). And I doubt
the cost of the extra hash of uint32 would be noticeable.

That being said the partitioning approach might be more sound and it's
definitely worth giving it a try.

Why would 11k bits be more than we'd like to store? Assuming we could
use the whole 8kB page for the bloom filter, that'd be about 64k bits.
In practice there'd be a bit of overhead (page header ...) but it's
still much more than 11k bits.

Brain fade -- I guess I thought we were avoiding being toasted, but
now I see that's not possible for BRIN storage. So, we'll want to
guard against this:

ERROR: index row size 8160 exceeds maximum 8152 for index "bar_num_idx"

While playing around with the numbers I had an epiphany: At the
defaults, the filter already takes up ~4.3kB, over half the page.
There is no room for another tuple, so if we're only indexing one
column, we might as well take up the whole page.

Hmm, yeah. I may be wrong but IIRC indexes don't support external
storage but compression is still allowed. So even if those defaults are
a bit higher than needed that should make the bloom filters a bit more
compressible, and thus fit multiple BRIN tuples on a single page.

Here MT = max tuples per 128 8k pages, or 37120, so default ndistinct
is 3712.

n k m p
MT/10 7 35580 0.01
MT/10 7 64000 0.0005
MT/10 12 64000 0.00025

Assuming ndistinct isn't way off reality, we get 20x-40x lower false
positive rate almost for free, and it'd be trivial to code! Keeping k
at 7 would be more robust, since it's equivalent to starting with n =
~6000, p = 0.006, which is still almost 2x less false positives than
you asked for. It also means nearly doubling the number of sorted
values before switching.

Going the other direction, capping nbits to 64k bits when ndistinct
gets too high, the false positive rate we can actually support starts
to drop. Here, the user requested 0.001 fpr.

n k p
4500 9 0.001
6000 7 0.006
7500 6 0.017
15000 3 0.129 (probably useless by now)
MT 1 0.440
64000 1 0.63 (possible with > 128 pages per range)

I imagine smaller pages_per_range settings are going to be useful for
skinny tables (note to self -- test). Maybe we could provide a way for
the user to see that their combination of pages_per_range, false
positive rate, and ndistinct is supportable, like
brin_bloom_get_supported_fpr(). Or document to check with page_inspect.
And that's not even considering multi-column indexes, like you
mentioned.

I agree giving users visibility into this would be useful.

Not sure about how much we want to rely on these optimizations, though,
considering multi-column indexes kinda break this.

But I guess we can simply make the table of primes a bit larger,
right?

If we want to support all the above cases without falling down
entirely, it would have to go up to 32k to be safe (When k = 1 we could
degenerate to one modulus on the whole filter). That would be a table
of about 7kB, which we could binary search. [thinks for a
moment]...Actually, if we wanted to be clever, maybe we could
precalculate the primes needed for the 64k bit cases and stick them at
the end of the array. The usual algorithm will find them. That way, we
could keep the array around 2kB. However, for >8kB block size, we
couldn't adjust the 64k number, which might be okay, but not really
project policy.

We could also generate the primes via a sieve instead, which is really
fast (and more code). That would be more robust, but that would require
the filter to store the actual primes used, so 20 more bytes at max k =
10. We could hard-code space for that, or to keep from hard-coding
maximum k and thus lowest possible false positive rate, we'd need more
bookkeeping.

I don't think the efficiency of this code matters too much - it's only
used once when creating the index, so the simpler the better. Certainly
for now, while testing the partitioning approach.

So, with the partition approach, we'd likely have to set in stone
either max nbits, or min target false positive rate. The latter option
seems more principled, not only for the block size, but also since the
target fp rate is already fixed by the reloption, and as I've
demonstrated, we can often go above and beyond the reloption even
without changing k.

That seems like a rather annoying limitation, TBH.

One wrinkle is that the sum of k primes is not going to match m
exactly. If the sum is too small, we can trim a few bits off of the
filter bitmap. If the sum is too large, the last partition can spill
into the front of the first one. This shouldn't matter much in the
common case since we need to round m to the nearest byte anyway.

AFAIK the paper simply says that as long as the sum of partitions is
close to the requested nbits, it's good enough. So I guess we could
just roll with that, no need to trim/wrap or something like that.

Hmm, I'm not sure I understand you. I can see not caring to trim wasted
bits, but we can't set/read off the end of the filter. If we don't
wrap, we could just skip reading/writing that bit. So a tiny portion of
access would be at k - 1. The paper is not clear on what to do here,
but they are explicit in minimizing the absolute value, which could go
on either side.

What I meant is that is that the paper says this:

Given a planned overall length mp for a Bloom filter, we usually
cannot get k prime numbers to make their sum mf to be exactly mp. As
long as the difference between mp and mf is small enough, it neither
causes any trouble for the software implementation nor noticeably
shifts the false positive ratio.

Which I think means we can pick mp, generate k primes with sum mf close
to mp, and just use that with mf bits.

Also I found a bug:

+ add_local_real_reloption(relopts, "false_positive_rate", + "desired
false-positive rate for the bloom filters", +
BLOOM_DEFAULT_FALSE_POSITIVE_RATE, + 0.001, 1.0, offsetof(BloomOptions,
falsePositiveRate));

When I set fp to 1.0, the reloption code is okay with that, but then
later the assertion gets triggered.

Hmm, yeah. I wonder what to do about that, considering indexes with fp
1.0 are essentially useless.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#108John Naylor
john.naylor@2ndquadrant.com
In reply to: Tomas Vondra (#107)
1 attachment(s)
Re: WIP: BRIN multi-range indexes

On Mon, Sep 21, 2020 at 3:56 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Mon, Sep 21, 2020 at 01:42:34PM -0400, John Naylor wrote:

While playing around with the numbers I had an epiphany: At the
defaults, the filter already takes up ~4.3kB, over half the page.
There is no room for another tuple, so if we're only indexing one
column, we might as well take up the whole page.

Hmm, yeah. I may be wrong but IIRC indexes don't support external
storage but compression is still allowed. So even if those defaults are
a bit higher than needed that should make the bloom filters a bit more
compressible, and thus fit multiple BRIN tuples on a single page.

Not sure about how much we want to rely on these optimizations, though,
considering multi-column indexes kinda break this.

Yeah. Okay, then it sounds like we should go in the other direction,
as the block comment at the top of brin_bloom.c implies. Indexes with
multiple bloom-indexed columns already don't fit in one 8kB page, so I
think every documented example should have a much lower
pages_per_range. Using 32 pages per range with max tuples gives n =
928. With default p, that's about 1.1 kB per brin tuple, so one brin
page can index 224 pages, much more than with the default 128.

Hmm, how ugly would it be to change the default range size depending
on the opclass?

If indexes don't support external storage, that sounds like a pain to
add. Also, with very small fpr, you can easily get into many megabytes
of filter space, which kind of defeats the purpose of brin in the
first place.

There is already this item from the brin readme:

* Different-size page ranges?
In the current design, each "index entry" in a BRIN index covers the same
number of pages. There's no hard reason for this; it might make sense to
allow the index to self-tune so that some index entries cover smaller page
ranges, if this allows the summary values to be more compact. This
would incur
larger BRIN overhead for the index itself, but might allow better pruning of
page ranges during scan. In the limit of one index tuple per page, the index
itself would occupy too much space, even though we would be able to skip
reading the most heap pages, because the summary values are tight; in the
opposite limit of a single tuple that summarizes the whole table, we wouldn't
be able to prune anything even though the index is very small. This can
probably be made to work by using the range map as an index in itself.

This sounds like a lot of work, but would be robust.

Anyway, given that this is a general problem and not specific to the
prime partition algorithm, I'll leave that out of the attached patch,
named as a .txt to avoid confusing the cfbot.

We could also generate the primes via a sieve instead, which is really
fast (and more code). That would be more robust, but that would require
the filter to store the actual primes used, so 20 more bytes at max k =
10. We could hard-code space for that, or to keep from hard-coding
maximum k and thus lowest possible false positive rate, we'd need more
bookkeeping.

I don't think the efficiency of this code matters too much - it's only
used once when creating the index, so the simpler the better. Certainly
for now, while testing the partitioning approach.

To check my understanding, isn't bloom_init() called for every tuple?
Agreed on simplicity so done this way.

So, with the partition approach, we'd likely have to set in stone
either max nbits, or min target false positive rate. The latter option
seems more principled, not only for the block size, but also since the
target fp rate is already fixed by the reloption, and as I've
demonstrated, we can often go above and beyond the reloption even
without changing k.

That seems like a rather annoying limitation, TBH.

I don't think the latter is that bad. I've capped k at 10 for
demonstration's sake.:

(928 is from using 32 pages per range)

n k m p
928 7 8895 0.01
928 10 13343 0.001 (lowest p supported in patch set)
928 13 17790 0.0001
928 10 18280 0.0001 (still works with lower k, needs higher m)
928 10 17790 0.00012 (even keeping m from row #3, capping k doesn't
degrade p much)

Also, k seems pretty robust against small changes as long as m isn't
artificially constrained and as long as p is small.

So I *think* it's okay to cap k at 10 or 12, and not bother with
adjusting m, which worsens space issues. As I found before, lowering k
raises target fpr, but seems more robust to overshooting ndistinct. In
any case, we only need k * 2 bytes to store the partition lengths.

The only way I see to avoid any limitation is to make the array of
primes variable length, which could be done by putting the filter
offset calculation into a macro. But having two variable-length arrays
seems messy.

Hmm, I'm not sure I understand you. I can see not caring to trim wasted
bits, but we can't set/read off the end of the filter. If we don't
wrap, we could just skip reading/writing that bit. So a tiny portion of
access would be at k - 1. The paper is not clear on what to do here,
but they are explicit in minimizing the absolute value, which could go
on either side.

What I meant is that is that the paper says this:

Given a planned overall length mp for a Bloom filter, we usually
cannot get k prime numbers to make their sum mf to be exactly mp. As
long as the difference between mp and mf is small enough, it neither
causes any trouble for the software implementation nor noticeably
shifts the false positive ratio.

Which I think means we can pick mp, generate k primes with sum mf close
to mp, and just use that with mf bits.

Oh, I see. When I said "trim" I meant exactly that (when mf < mp).
Yeah, we can bump it up as well for the other case. I've done it that
way.

+ add_local_real_reloption(relopts, "false_positive_rate", + "desired
false-positive rate for the bloom filters", +
BLOOM_DEFAULT_FALSE_POSITIVE_RATE, + 0.001, 1.0, offsetof(BloomOptions,
falsePositiveRate));

When I set fp to 1.0, the reloption code is okay with that, but then
later the assertion gets triggered.

Hmm, yeah. I wonder what to do about that, considering indexes with fp
1.0 are essentially useless.

Not just useless -- they're degenerate. When p = 1.0, m = k = 0 -- We
cannot accept this value from the user. Looking up thread, 0.1 was
suggested as a limit. That might be a good starting point.

This is interesting work! Having gone this far, I'm going to put more
attention to the multi-minmax patch and actually start performance
testing.

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

one-hash-bloom-filter-20200917b.txttext/plain; charset=US-ASCII; name=one-hash-bloom-filter-20200917b.txtDownload
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 03e9d4b713..5dc213727b 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -203,6 +203,9 @@ typedef struct BloomOptions
  */
 #define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
 
+/* With the minimum allowed false positive rate of 0.001, we need up to 10 hashes */
+#define		BLOOM_MAX_NUM_PARTITIONS	10
+
 #define BloomGetNDistinctPerRange(opts) \
 	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
 	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
@@ -270,6 +273,7 @@ typedef struct BloomFilter
 	uint8	nhashes;	/* number of hash functions */
 	uint32	nbits;		/* number of bits in the bitmap (size) */
 	uint32	nbits_set;	/* number of bits set to 1 */
+	uint16	partlens[BLOOM_MAX_NUM_PARTITIONS];	/* partition lengths */
 
 	/* data of the bloom filter (used both for sorted and hashed phase) */
 	char	data[FLEXIBLE_ARRAY_MEMBER];
@@ -279,6 +283,114 @@ typedef struct BloomFilter
 static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
 
 
+/*
+ * generate_primes
+ * 		returns array of all primes less than limit
+ *
+ * WIP: very naive prime sieve; could be optimized using segmented ranges
+ */
+static uint16 *
+generate_primes(int limit)
+{
+	/* upper bound of number of primes below limit */
+	/* WIP: reference for this number */
+	int numprimes = 1.26 * limit / log(limit);
+
+	bool *is_composite = (bool *) palloc0(limit * sizeof(bool));
+	uint16 *primes = (uint16 *) palloc0(numprimes * sizeof(uint16));
+
+	int maxfactor = floor(sqrt(limit));
+	int factor = 2;	/* first prime */
+
+	/* mark the sieve where the index is composite */
+	while (factor < maxfactor)
+	{
+		for (int i = factor * factor; i < limit; i += factor)
+			 is_composite[i] = true;
+		do { factor++; } while (is_composite[factor]);
+	}
+
+	/* the unmarked numbers are prime, so copy over */
+	for (int i = 2, j = 0; i < limit && j < numprimes; i++)
+	{
+		if (!is_composite[i])
+			primes[j++] = i;
+	}
+
+	/* there should still be some zeroes at the end, but make sure */
+	primes[numprimes - 1] = 0;
+
+	/* pretty large, so free it now (segmented ranges would make it smaller) */
+	pfree(is_composite);
+	return primes;
+}
+
+/*
+ * set_bloom_partitions
+ * 		Calculate k moduli for one-hashing bloom filter.
+ *
+ * Find consecutive primes whose sum is close to nbits and
+ * return the sum. Copy the primes to the filter to use as
+ * partition lengths.
+ * WIP: one-hashing bf paper ref somewhere
+ */
+static uint32
+set_bloom_partitions(int nbits, uint8 nhashes, uint16 *partlens)
+{
+	int		min, diff, incr;
+	int		pidx = 0;
+	int		sum = 0;
+	int		target_partlen = nbits / nhashes;
+
+	/*
+	 * Increase the limit to ensure we have some primes higher than
+	 * the target partition length. The 100 value is arbitrary, but
+	 * should be well over what we need.
+	 */
+	uint16 *primes = generate_primes(target_partlen + 100);
+
+	/*
+	 * In our array of primes, find a sequence of length nhashes, whose
+	 * last item is close to our target partition length. The end of the
+	 * array will be filled with zeros, so we need to guard against that.
+	 */
+	while (primes[pidx + nhashes - 1] <= target_partlen &&
+		   primes[pidx] > 0)
+		pidx++;
+
+	for (int i = 0; i < nhashes; i++)
+		sum += primes[pidx + i];
+
+	/*
+	 * Since all the primes are less than or equal the desired partition
+	 * length, the sum is somewhat less than nbits. Increment the starting
+	 * point until we find the sequence of primes whose sum is closest to
+	 * nbits. It doesn't matter whether it's higher or lower.
+	 */
+	min = abs(nbits - sum);
+	for (;;)
+	{
+		incr = primes[pidx + nhashes] - primes[pidx];
+		diff = abs(nbits - (sum + incr));
+		if (diff >= min)
+			break;
+
+		min = diff;
+		sum += incr;
+		pidx++;
+	}
+
+	memcpy(partlens, &primes[pidx], nhashes * sizeof(uint16));
+
+	/* WIP: assuming it's not important to pfree primes */
+
+	/*
+	 * The actual filter length will be the sum of the partition lengths
+	 * rounded up to the nearest byte.
+	 */
+	return (uint32) ((sum + 7) / 8) * 8;
+}
+
 /*
  * bloom_init
  * 		Initialize the Bloom Filter, allocate all the memory.
@@ -301,15 +413,13 @@ bloom_init(int ndistinct, double false_positive_rate)
 
 	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
 
-	/* round m to whole bytes */
-	m = ((m + 7) / 8) * 8;
-
 	/*
 	 * round(log(2.0) * m / ndistinct), but assume round() may not be
 	 * available on Windows
 	 */
 	k = log(2.0) * m / ndistinct;
 	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+	k = Min(k, BLOOM_MAX_NUM_PARTITIONS);
 
 	/*
 	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
@@ -323,7 +433,9 @@ bloom_init(int ndistinct, double false_positive_rate)
 
 	filter->flags = 0;	/* implies SORTED phase */
 	filter->nhashes = (int) k;
-	filter->nbits = m;
+
+	/* calculate the partition lengths and adjust m to match */
+	filter->nbits = set_bloom_partitions(m, k, filter->partlens);
 
 	SET_VARSIZE(filter, len);
 
@@ -445,7 +557,7 @@ static BloomFilter *
 bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 {
 	int		i;
-	uint32	big_h, h, d;
+	int		part_boundary = 0;
 
 	/* assume 'not updated' by default */
 	Assert(filter);
@@ -514,17 +626,16 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 	/* we better be in the hashing phase */
 	Assert(BLOOM_IS_HASHED(filter));
 
-	/* compute the hashes, used for the bloom filter */
-	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
-
-	h = big_h % filter->nbits;
-	d = big_h % (filter->nbits - 1);
-
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		int byte = (h / 8);
-		int bit  = (h % 8);
+		int partlen = filter->partlens[i];
+		int bitloc = part_boundary + (value % partlen);
+
+		Assert(bitloc < filter->nbits);
+
+		int byte = (bitloc / 8);
+		int bit  = (bitloc % 8);
 
 		/* if the bit is not set, set it and remember we did that */
 		if (! (filter->data[byte] & (0x01 << bit)))
@@ -536,12 +647,7 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 		}
 
 		/* next bit */
-		h += d++;
-		if (h >= filter->nbits)
-			h -= filter->nbits;
-
-		if (d == filter->nbits)
-			d = 0;
+		part_boundary += partlen;
 	}
 
 	return filter;
@@ -571,6 +677,7 @@ bloom_switch_to_hashing(BloomFilter *filter)
 
 	newfilter->nhashes = filter->nhashes;
 	newfilter->nbits = filter->nbits;
+	memcpy(newfilter->partlens, filter->partlens, filter->nhashes * sizeof(uint16));
 	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
 
 	SET_VARSIZE(newfilter, len);
@@ -595,7 +702,7 @@ static bool
 bloom_contains_value(BloomFilter *filter, uint32 value)
 {
 	int		i;
-	uint32	big_h, h, d;
+	int		part_boundary = 0;
 
 	Assert(filter);
 
@@ -624,28 +731,23 @@ bloom_contains_value(BloomFilter *filter, uint32 value)
 	/* now the regular hashing mode */
 	Assert(BLOOM_IS_HASHED(filter));
 
-	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
-
-	h = big_h % filter->nbits;
-	d = big_h % (filter->nbits - 1);
-
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		int byte = (h / 8);
-		int bit  = (h % 8);
+		int partlen = filter->partlens[i];
+		int bitloc = part_boundary + (value % partlen);
+
+		Assert(bitloc < filter->nbits);
+
+		int byte = (bitloc / 8);
+		int bit  = (bitloc % 8);
 
 		/* if the bit is not set, the value is not there */
 		if (! (filter->data[byte] & (0x01 << bit)))
 			return false;
 
 		/* next bit */
-		h += d++;
-		if (h >= filter->nbits)
-			h -= filter->nbits;
-
-		if (d == filter->nbits)
-			d = 0;
+		part_boundary += partlen;
 	}
 
 	/* all hashes found in bloom filter */
@@ -1065,8 +1167,14 @@ brin_bloom_summary_out(PG_FUNCTION_ARGS)
 
 	if (BLOOM_IS_HASHED(filter))
 	{
-		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+		appendStringInfo(&str,
+						 "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u  partition lengths:  [",
 						 filter->nhashes, filter->nbits, filter->nbits_set);
+		for (int i = 0; i < filter->nhashes - 1; i++)
+		{
+			appendStringInfo(&str, "%u, ", filter->partlens[i]);
+		}
+		appendStringInfo(&str, "%u]", filter->partlens[filter->nhashes - 1]);
 	}
 	else
 	{
#109Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: John Naylor (#108)
Re: WIP: BRIN multi-range indexes

On Thu, Sep 24, 2020 at 05:18:03PM -0400, John Naylor wrote:

On Mon, Sep 21, 2020 at 3:56 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Mon, Sep 21, 2020 at 01:42:34PM -0400, John Naylor wrote:

While playing around with the numbers I had an epiphany: At the
defaults, the filter already takes up ~4.3kB, over half the page.
There is no room for another tuple, so if we're only indexing one
column, we might as well take up the whole page.

Hmm, yeah. I may be wrong but IIRC indexes don't support external
storage but compression is still allowed. So even if those defaults are
a bit higher than needed that should make the bloom filters a bit more
compressible, and thus fit multiple BRIN tuples on a single page.

Not sure about how much we want to rely on these optimizations, though,
considering multi-column indexes kinda break this.

Yeah. Okay, then it sounds like we should go in the other direction,
as the block comment at the top of brin_bloom.c implies. Indexes with
multiple bloom-indexed columns already don't fit in one 8kB page, so I
think every documented example should have a much lower
pages_per_range. Using 32 pages per range with max tuples gives n =
928. With default p, that's about 1.1 kB per brin tuple, so one brin
page can index 224 pages, much more than with the default 128.

Hmm, how ugly would it be to change the default range size depending
on the opclass?

Not sure. What would happen for multi-column BRIN indexes with different
opclasses?

If indexes don't support external storage, that sounds like a pain to
add. Also, with very small fpr, you can easily get into many megabytes
of filter space, which kind of defeats the purpose of brin in the
first place.

True.

There is already this item from the brin readme:

* Different-size page ranges?
In the current design, each "index entry" in a BRIN index covers the same
number of pages. There's no hard reason for this; it might make sense to
allow the index to self-tune so that some index entries cover smaller page
ranges, if this allows the summary values to be more compact. This
would incur
larger BRIN overhead for the index itself, but might allow better pruning of
page ranges during scan. In the limit of one index tuple per page, the index
itself would occupy too much space, even though we would be able to skip
reading the most heap pages, because the summary values are tight; in the
opposite limit of a single tuple that summarizes the whole table, we wouldn't
be able to prune anything even though the index is very small. This can
probably be made to work by using the range map as an index in itself.

This sounds like a lot of work, but would be robust.

Yeah. I think it's a fairly independent / orthogonal project.

Anyway, given that this is a general problem and not specific to the
prime partition algorithm, I'll leave that out of the attached patch,
named as a .txt to avoid confusing the cfbot.

We could also generate the primes via a sieve instead, which is really
fast (and more code). That would be more robust, but that would require
the filter to store the actual primes used, so 20 more bytes at max k =
10. We could hard-code space for that, or to keep from hard-coding
maximum k and thus lowest possible false positive rate, we'd need more
bookkeeping.

I don't think the efficiency of this code matters too much - it's only
used once when creating the index, so the simpler the better. Certainly
for now, while testing the partitioning approach.

To check my understanding, isn't bloom_init() called for every tuple?
Agreed on simplicity so done this way.

No, it's only called for the first non-NULL value in the page range
(unless I made a boo boo when writing that code).

So, with the partition approach, we'd likely have to set in stone
either max nbits, or min target false positive rate. The latter option
seems more principled, not only for the block size, but also since the
target fp rate is already fixed by the reloption, and as I've
demonstrated, we can often go above and beyond the reloption even
without changing k.

That seems like a rather annoying limitation, TBH.

I don't think the latter is that bad. I've capped k at 10 for
demonstration's sake.:

(928 is from using 32 pages per range)

n k m p
928 7 8895 0.01
928 10 13343 0.001 (lowest p supported in patch set)
928 13 17790 0.0001
928 10 18280 0.0001 (still works with lower k, needs higher m)
928 10 17790 0.00012 (even keeping m from row #3, capping k doesn't
degrade p much)

Also, k seems pretty robust against small changes as long as m isn't
artificially constrained and as long as p is small.

So I *think* it's okay to cap k at 10 or 12, and not bother with
adjusting m, which worsens space issues. As I found before, lowering k
raises target fpr, but seems more robust to overshooting ndistinct. In
any case, we only need k * 2 bytes to store the partition lengths.

The only way I see to avoid any limitation is to make the array of
primes variable length, which could be done by putting the filter
offset calculation into a macro. But having two variable-length arrays
seems messy.

Hmmm. I wonder how would these limitations impact the conclusions from
the one-hashing paper? Or was this just for the sake of a demonstration?

I'd suggest we just do the simplest thing possible (be it a hard-coded
table of primes or a sieve) and then evaluate if we need to do something
more sophisticated.

Hmm, I'm not sure I understand you. I can see not caring to trim wasted
bits, but we can't set/read off the end of the filter. If we don't
wrap, we could just skip reading/writing that bit. So a tiny portion of
access would be at k - 1. The paper is not clear on what to do here,
but they are explicit in minimizing the absolute value, which could go
on either side.

What I meant is that is that the paper says this:

Given a planned overall length mp for a Bloom filter, we usually
cannot get k prime numbers to make their sum mf to be exactly mp. As
long as the difference between mp and mf is small enough, it neither
causes any trouble for the software implementation nor noticeably
shifts the false positive ratio.

Which I think means we can pick mp, generate k primes with sum mf close
to mp, and just use that with mf bits.

Oh, I see. When I said "trim" I meant exactly that (when mf < mp).
Yeah, we can bump it up as well for the other case. I've done it that
way.

OK

+ add_local_real_reloption(relopts, "false_positive_rate", + "desired
false-positive rate for the bloom filters", +
BLOOM_DEFAULT_FALSE_POSITIVE_RATE, + 0.001, 1.0, offsetof(BloomOptions,
falsePositiveRate));

When I set fp to 1.0, the reloption code is okay with that, but then
later the assertion gets triggered.

Hmm, yeah. I wonder what to do about that, considering indexes with fp
1.0 are essentially useless.

Not just useless -- they're degenerate. When p = 1.0, m = k = 0 -- We
cannot accept this value from the user. Looking up thread, 0.1 was
suggested as a limit. That might be a good starting point.

Makes sense, I'll fix it that way.

This is interesting work! Having gone this far, I'm going to put more
attention to the multi-minmax patch and actually start performance
testing.

Cool, thanks! I'll take a look at your one-hashing patch tomorrow.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#110John Naylor
john.naylor@2ndquadrant.com
In reply to: Tomas Vondra (#109)
Re: WIP: BRIN multi-range indexes

On Thu, Sep 24, 2020 at 7:50 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Thu, Sep 24, 2020 at 05:18:03PM -0400, John Naylor wrote:

Hmm, how ugly would it be to change the default range size depending
on the opclass?

Not sure. What would happen for multi-column BRIN indexes with different
opclasses?

Sounds like a can of worms. In any case I suspect if there is no more
graceful way to handle too-large filters than ERROR out the first time
trying to write to the index, this feature might meet some resistance.
Not sure what to suggest, though.

I don't think the efficiency of this code matters too much - it's only
used once when creating the index, so the simpler the better. Certainly
for now, while testing the partitioning approach.

To check my understanding, isn't bloom_init() called for every tuple?
Agreed on simplicity so done this way.

No, it's only called for the first non-NULL value in the page range
(unless I made a boo boo when writing that code).

Ok, then I basically understood -- by tuple I meant BRIN tuple, pardon
my ambiguity. After thinking about it, I agree that CPU cost is
probably trivial (and if not, something is seriously wrong).

n k m p
928 7 8895 0.01
928 10 13343 0.001 (lowest p supported in patch set)
928 13 17790 0.0001
928 10 18280 0.0001 (still works with lower k, needs higher m)
928 10 17790 0.00012 (even keeping m from row #3, capping k doesn't
degrade p much)

Also, k seems pretty robust against small changes as long as m isn't
artificially constrained and as long as p is small.

So I *think* it's okay to cap k at 10 or 12, and not bother with
adjusting m, which worsens space issues. As I found before, lowering k
raises target fpr, but seems more robust to overshooting ndistinct. In
any case, we only need k * 2 bytes to store the partition lengths.

The only way I see to avoid any limitation is to make the array of
primes variable length, which could be done by putting the filter
offset calculation into a macro. But having two variable-length arrays
seems messy.

Hmmm. I wonder how would these limitations impact the conclusions from
the one-hashing paper? Or was this just for the sake of a demonstration?

Using "10" in the patch is a demonstration, which completely supports
the current fpr allowed by the reloption, and showing what happens if
fpr is allowed to go lower. But for your question, I *think* this
consideration is independent from the conclusions. The n, k, m values
give a theoretical false positive rate, assuming a completely perfect
hashing scheme. The numbers I'm playing with show consequences in the
theoretical fpr. The point of the paper (and others like it) is how to
get the real fpr as close as possible to the fpr predicted by the
theory. My understanding anyway.

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#111Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: John Naylor (#110)
Re: WIP: BRIN multi-range indexes

On Mon, Sep 28, 2020 at 04:42:39PM -0400, John Naylor wrote:

On Thu, Sep 24, 2020 at 7:50 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On Thu, Sep 24, 2020 at 05:18:03PM -0400, John Naylor wrote:

Hmm, how ugly would it be to change the default range size depending
on the opclass?

Not sure. What would happen for multi-column BRIN indexes with different
opclasses?

Sounds like a can of worms. In any case I suspect if there is no more
graceful way to handle too-large filters than ERROR out the first time
trying to write to the index, this feature might meet some resistance.
Not sure what to suggest, though.

Is it actually all that different from the existing BRIN indexes?
Consider this example:

create table x (a text, b text, c text);

create index on x using brin (a,b,c);

create or replace function random_str(p_len int) returns text as $$
select string_agg(x, '') from (select chr(1 + (254 * random())::int ) as x from generate_series(1,$1)) foo;
$$ language sql;

test=# insert into x select random_str(1000), random_str(1000), random_str(1000);
ERROR: index row size 9056 exceeds maximum 8152 for index "x_a_b_c_idx"

I'm a bit puzzled, though, because both of these things seem to work:

1) insert before creating the index

create table x (a text, b text, c text);
insert into x select random_str(1000), random_str(1000), random_str(1000);
create index on x using brin (a,b,c);
-- and there actually is a non-empty summary with real data
select * from brin_page_items(get_raw_page('x_a_b_c_idx', 2), 'x_a_b_c_idx'::regclass);

2) insert "small" row before inserting the over-sized one

create table x (a text, b text, c text);
insert into x select random_str(10), random_str(10), random_str(10);
insert into x select random_str(1000), random_str(1000), random_str(1000);
create index on x using brin (a,b,c);
-- and there actually is a non-empty summary with the "big" values
select * from brin_page_items(get_raw_page('x_a_b_c_idx', 2), 'x_a_b_c_idx'::regclass);

I find this somewhat strange - how come we don't fail here too?

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#112John Naylor
john.naylor@2ndquadrant.com
In reply to: Tomas Vondra (#111)
Re: WIP: BRIN multi-range indexes

On Mon, Sep 28, 2020 at 10:12 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Is it actually all that different from the existing BRIN indexes?
Consider this example:

create table x (a text, b text, c text);

create index on x using brin (a,b,c);

create or replace function random_str(p_len int) returns text as $$
select string_agg(x, '') from (select chr(1 + (254 * random())::int ) as x from generate_series(1,$1)) foo;
$$ language sql;

test=# insert into x select random_str(1000), random_str(1000), random_str(1000);
ERROR: index row size 9056 exceeds maximum 8152 for index "x_a_b_c_idx"

Hmm, okay. As for which comes first, insert or index creation, I'm
baffled, too. I also would expect the example above would take up a
bit over 6000 bytes, but not 9000.

--
John Naylor https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#113Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: John Naylor (#112)
Re: WIP: BRIN multi-range indexes

On Wed, Sep 30, 2020 at 07:57:19AM -0400, John Naylor wrote:

On Mon, Sep 28, 2020 at 10:12 PM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Is it actually all that different from the existing BRIN indexes?
Consider this example:

create table x (a text, b text, c text);

create index on x using brin (a,b,c);

create or replace function random_str(p_len int) returns text as $$
select string_agg(x, '') from (select chr(1 + (254 * random())::int ) as x from generate_series(1,$1)) foo;
$$ language sql;

test=# insert into x select random_str(1000), random_str(1000), random_str(1000);
ERROR: index row size 9056 exceeds maximum 8152 for index "x_a_b_c_idx"

Hmm, okay. As for which comes first, insert or index creation, I'm
baffled, too. I also would expect the example above would take up a
bit over 6000 bytes, but not 9000.

OK, so this seems like a data corruption bug in BRIN, actually.

The ~9000 bytes is actually about right, because the strings are in
UTF-8 so roughly 1.5B per character seems about right. And we have 6
values to store (3 columns, min/max for each), so 6 * 1500 = 9000.

The real question is how come INSERT + CREATE INDEX actually manages to
create an index tuple. And the answer is pretty simple - brin_form_tuple
kinda ignores toasting, happily building index tuples where some values
are toasted.

Consider this:

create table x (a text, b text, c text);
insert into x select random_str(1000), random_str(1000), random_str(1000);

create index on x using brin (a,b,c);
delete from x;
vacuum x;

set enable_seqscan=off;

insert into x select random_str(10), random_str(10), random_str(10);
ERROR: missing chunk number 0 for toast value 16530 in pg_toast_16525

explain analyze select * from x where a = 'xxx';
ERROR: missing chunk number 0 for toast value 16530 in pg_toast_16525

select * from brin_page_items(get_raw_page('x_a_b_c_idx', 2), 'x_a_b_c_idx'::regclass);
ERROR: missing chunk number 0 for toast value 16547 in pg_toast_16541

Interestingly enough, running the select before the insert seems to be
working - not sure why.

Anyway, it behaves like this since 9.5 :-(

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#114Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Tomas Vondra (#113)
Re: WIP: BRIN multi-range indexes

On 2020-Oct-01, Tomas Vondra wrote:

OK, so this seems like a data corruption bug in BRIN, actually.

Oh crap. You're right -- the data needs to be detoasted before being
put in the index.

I'll have a look at how this can be fixed.

#115Anastasia Lubennikova
a.lubennikova@postgrespro.ru
In reply to: Alvaro Herrera (#114)
Re: WIP: BRIN multi-range indexes

Status update for a commitfest entry.

According to cfbot the patch no longer compiles.
Tomas, can you send an update, please?

I also see that a few last messages mention a data corruption bug. Sounds pretty serious.
Alvaro, have you had a chance to look at it? I don't see anything committed yet, nor any active discussion in other threads.

#116Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Anastasia Lubennikova (#115)
7 attachment(s)
Re: WIP: BRIN multi-range indexes

On Mon, Nov 02, 2020 at 06:05:27PM +0000, Anastasia Lubennikova wrote:

Status update for a commitfest entry.

According to cfbot the patch no longer compiles. Tomas, can you send
an update, please?

Yep, here's an updated patch series. It got broken by f90149e6285aa
which disallowed OID macros in pg_type, but fixing it was simple.

I've also included the patch adopting the one-hash bloom, as implemented
by John Naylor. I didn't have time to do any testing / evaluation yet,
so I've kept it as a separate part - ultimately we should either merge
it into the other bloom patch or discard it.

I also see that a few last messages mention a data corruption bug.
Sounds pretty serious. Alvaro, have you had a chance to look at it? I
don't see anything committed yet, nor any active discussion in other
threads.

Yeah, I'm not aware of any fix addressing this - my understanding was
Alvaro plans to handle that, but amybe I misinterpreted his response.
Anyway, I think the fix is simple - we need to de-TOAST the data while
adding the data to index, and we need to consider what to do with
existing possibly-broken indexes.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20201103.patchtext/plain; charset=us-asciiDownload
From bd8a32967b41c4c4efc0b7df54650c82a683e357 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/7] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1f72562c60..ccf609e799 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +528,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,51 +538,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..853727190c 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +443,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..b233558310 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d9770bbadd..8f0f76601b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8123,7 +8123,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8139,7 +8139,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20201103.patchtext/plain; charset=us-asciiDownload
From 18195ac9ded163bac8fdf0d32609082e283b62b2 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/7] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit, and it simplifies the support
procedures quite a bit, as they don't need to care about NULL values and
flags at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 294 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 240 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index ccf609e799..f438e59c75 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -359,6 +337,7 @@ brinbeginscan(Relation r, int nkeys, int norderbys)
 	return scan;
 }
 
+
 /*
  * Execute the index scan.
  *
@@ -389,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +396,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,23 +423,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -463,9 +448,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,15 +542,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -694,7 +735,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -723,25 +763,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1520,6 +1543,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1573,3 +1629,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 853727190c..f5eaef416f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -574,37 +506,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index b233558310..8886ad984e 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0003-optimize-allocations-20201103.patchtext/plain; charset=us-asciiDownload
From 58f920a5346420eb93934443983794a286391dd3 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/7] optimize allocations

---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index f438e59c75..bd052c8b0b 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -398,11 +401,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -438,9 +480,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -451,17 +493,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0004-BRIN-bloom-indexes-20201103.patchtext/plain; charset=us-asciiDownload
From a5ad2fbd4b7e3a1797e6a8903c55e99dd283a29b Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:43 +0200
Subject: [PATCH 4/7] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    |  178 ++++
 doc/src/sgml/ref/create_index.sgml        |   31 +
 src/backend/access/brin/Makefile          |    1 +
 src/backend/access/brin/brin_bloom.c      | 1107 +++++++++++++++++++++
 src/include/access/brin.h                 |    2 +
 src/include/access/brin_internal.h        |    4 +
 src/include/catalog/pg_amop.dat           |  170 ++++
 src/include/catalog/pg_amproc.dat         |  447 +++++++++
 src/include/catalog/pg_opclass.dat        |   72 ++
 src/include/catalog/pg_opfamily.dat       |   38 +
 src/include/catalog/pg_proc.dat           |   34 +
 src/include/catalog/pg_type.dat           |    7 +
 src/test/regress/expected/brin_bloom.out  |  456 +++++++++
 src/test/regress/expected/opr_sanity.out  |    3 +-
 src/test/regress/expected/psql.out        |    3 +-
 src/test/regress/expected/type_sanity.out |    7 +-
 src/test/regress/parallel_schedule        |    5 +
 src/test/regress/serial_schedule          |    1 +
 src/test/regress/sql/brin_bloom.sql       |  404 ++++++++
 19 files changed, 2965 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 749db2845e..fea13791d6 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -559,6 +559,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..977d11d790
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,1107 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * The 1% value is mostly arbitrary, it just looks nice.
+ */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i, j, k;
+	Size	len;
+
+	uint32 *values;
+	uint32 *result;
+	uint32 *sorted;
+	uint32 *unsorted;
+
+	int		nvalues;
+	int		nsorted;
+	int		nunsorted;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* when already fully sorted, no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* result of merging */
+	k = 0;
+	result = (uint32 *) palloc(sizeof(uint32) * filter->nvalues);
+
+	/* first we have sorted data, then unsorted */
+	sorted = values;
+	nsorted = filter->nsorted;
+
+	unsorted = &values[filter->nsorted];
+	nunsorted = filter->nvalues - filter->nsorted;
+
+	/* sort the unsorted part, then merge the two parts */
+	pg_qsort(unsorted, nunsorted, sizeof(uint32), cmp_uint32);
+
+	i = 0;	/* sorted index */
+	j = 0;	/* unsorted index */
+
+	while ((i < nsorted) && (j < nunsorted))
+	{
+		if (sorted[i] <= unsorted[j])
+			result[k++] = sorted[i++];
+		else
+			result[k++] = unsorted[j++];
+	}
+
+	while (i < nsorted)
+		result[k++] = sorted[i++];
+
+	while (j < nunsorted)
+		result[k++] = unsorted[j++];
+
+	/* compact - remove duplicate values */
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (result[i] != result[nvalues - 1])
+			result[nvalues++] = result[i];
+	}
+
+	memcpy(filter->data, result, sizeof(uint32) * nvalues);
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 0.001, 1.0, offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 1dfb6fd373..12033d48ec 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1705,6 +1705,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1722,6 +1727,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1739,6 +1749,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1885,6 +1900,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1902,6 +1955,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1919,6 +1977,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1936,6 +1999,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2002,6 +2070,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2019,6 +2101,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2036,6 +2123,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2053,6 +2145,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2090,6 +2187,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2107,6 +2209,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2253,6 +2360,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2270,6 +2415,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2287,6 +2437,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2338,6 +2493,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2355,6 +2515,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2416,6 +2581,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index a8e0c4ff8a..15f3baea15 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -772,6 +772,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -785,6 +803,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -798,6 +834,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -899,6 +953,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -912,6 +1018,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -924,6 +1048,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -936,6 +1077,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -986,6 +1144,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1000,6 +1197,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1014,6 +1231,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1027,6 +1264,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1061,6 +1316,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1074,6 +1349,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1181,6 +1474,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1195,6 +1544,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1209,6 +1578,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1249,6 +1638,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1262,6 +1671,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1297,6 +1724,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..ca747a03b9 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +389,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..8875079698 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 8f0f76601b..c746efd33b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8146,6 +8146,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -10999,3 +11019,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 21a467a7a7..59da6a9804 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -621,3 +621,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..d58a4a483c
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+ERROR:  value 0.0 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+ERROR:  value 1.01 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "1.000000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 7825a765cd..ef0f19f9f6 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2021,6 +2021,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2084,7 +2085,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(125 rows)
+(126 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index daac0ff49d..72c7598c89 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4989,8 +4989,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 7a46a13252..a8e677dd0b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 9a80b80f73..5776dbdd73 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..abd5a33deb
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 1.01)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0005-use-one-hash-bloom-variant-20201103.patchtext/plain; charset=us-asciiDownload
From b33e0c4e93226ba665b6087a5b86c348079fdbbc Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Mon, 2 Nov 2020 23:55:28 +0100
Subject: [PATCH 5/7] use one-hash bloom variant

---
 src/backend/access/brin/brin_bloom.c | 176 +++++++++++++++++++++------
 1 file changed, 142 insertions(+), 34 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 977d11d790..b5f07954a6 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -203,6 +203,9 @@ typedef struct BloomOptions
  */
 #define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
 
+/* With the minimum allowed false positive rate of 0.001, we need up to 10 hashes */
+#define		BLOOM_MAX_NUM_PARTITIONS	10
+
 #define BloomGetNDistinctPerRange(opts) \
 	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
 	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
@@ -270,6 +273,7 @@ typedef struct BloomFilter
 	uint8	nhashes;	/* number of hash functions */
 	uint32	nbits;		/* number of bits in the bitmap (size) */
 	uint32	nbits_set;	/* number of bits set to 1 */
+	uint16	partlens[BLOOM_MAX_NUM_PARTITIONS];	/* partition lengths */
 
 	/* data of the bloom filter (used both for sorted and hashed phase) */
 	char	data[FLEXIBLE_ARRAY_MEMBER];
@@ -279,6 +283,114 @@ typedef struct BloomFilter
 static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
 
 
+/*
+ * generate_primes
+ * 		returns array of all primes less than limit
+ *
+ * WIP: very naive prime sieve; could be optimized using segmented ranges
+ */
+static uint16 *
+generate_primes(int limit)
+{
+	/* upper bound of number of primes below limit */
+	/* WIP: reference for this number */
+	int numprimes = 1.26 * limit / log(limit);
+
+	bool *is_composite = (bool *) palloc0(limit * sizeof(bool));
+	uint16 *primes = (uint16 *) palloc0(numprimes * sizeof(uint16));
+
+	int maxfactor = floor(sqrt(limit));
+	int factor = 2;	/* first prime */
+
+	/* mark the sieve where the index is composite */
+	while (factor < maxfactor)
+	{
+		for (int i = factor * factor; i < limit; i += factor)
+			 is_composite[i] = true;
+		do { factor++; } while (is_composite[factor]);
+	}
+
+	/* the unmarked numbers are prime, so copy over */
+	for (int i = 2, j = 0; i < limit && j < numprimes; i++)
+	{
+		if (!is_composite[i])
+			primes[j++] = i;
+	}
+
+	/* there should still be some zeroes at the end, but make sure */
+	primes[numprimes - 1] = 0;
+
+	/* pretty large, so free it now (segmented ranges would make it smaller) */
+	pfree(is_composite);
+	return primes;
+}
+
+/*
+ * set_bloom_partitions
+ * 		Calculate k moduli for one-hashing bloom filter.
+ *
+ * Find consecutive primes whose sum is close to nbits and
+ * return the sum. Copy the primes to the filter to use as
+ * partition lengths.
+ * WIP: one-hashing bf paper ref somewhere
+ */
+static uint32
+set_bloom_partitions(int nbits, uint8 nhashes, uint16 *partlens)
+{
+	int		min, diff, incr;
+	int		pidx = 0;
+	int		sum = 0;
+	int		target_partlen = nbits / nhashes;
+
+	/*
+	 * Increase the limit to ensure we have some primes higher than
+	 * the target partition length. The 100 value is arbitrary, but
+	 * should be well over what we need.
+	 */
+	uint16 *primes = generate_primes(target_partlen + 100);
+
+	/*
+	 * In our array of primes, find a sequence of length nhashes, whose
+	 * last item is close to our target partition length. The end of the
+	 * array will be filled with zeros, so we need to guard against that.
+	 */
+	while (primes[pidx + nhashes - 1] <= target_partlen &&
+		   primes[pidx] > 0)
+		pidx++;
+
+	for (int i = 0; i < nhashes; i++)
+		sum += primes[pidx + i];
+
+	/*
+	 * Since all the primes are less than or equal the desired partition
+	 * length, the sum is somewhat less than nbits. Increment the starting
+	 * point until we find the sequence of primes whose sum is closest to
+	 * nbits. It doesn't matter whether it's higher or lower.
+	 */
+	min = abs(nbits - sum);
+	for (;;)
+	{
+		incr = primes[pidx + nhashes] - primes[pidx];
+		diff = abs(nbits - (sum + incr));
+		if (diff >= min)
+			break;
+
+		min = diff;
+		sum += incr;
+		pidx++;
+	}
+
+	memcpy(partlens, &primes[pidx], nhashes * sizeof(uint16));
+
+	/* WIP: assuming it's not important to pfree primes */
+
+	/*
+	 * The actual filter length will be the sum of the partition lengths
+	 * rounded up to the nearest byte.
+	 */
+	return (uint32) ((sum + 7) / 8) * 8;
+}
+
 /*
  * bloom_init
  * 		Initialize the Bloom Filter, allocate all the memory.
@@ -301,15 +413,13 @@ bloom_init(int ndistinct, double false_positive_rate)
 
 	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
 
-	/* round m to whole bytes */
-	m = ((m + 7) / 8) * 8;
-
 	/*
 	 * round(log(2.0) * m / ndistinct), but assume round() may not be
 	 * available on Windows
 	 */
 	k = log(2.0) * m / ndistinct;
 	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+	k = Min(k, BLOOM_MAX_NUM_PARTITIONS);
 
 	/*
 	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
@@ -323,7 +433,9 @@ bloom_init(int ndistinct, double false_positive_rate)
 
 	filter->flags = 0;	/* implies SORTED phase */
 	filter->nhashes = (int) k;
-	filter->nbits = m;
+
+	/* calculate the partition lengths and adjust m to match */
+	filter->nbits = set_bloom_partitions(m, k, filter->partlens);
 
 	SET_VARSIZE(filter, len);
 
@@ -445,7 +557,7 @@ static BloomFilter *
 bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 {
 	int		i;
-	uint32	big_h, h, d;
+	int		part_boundary = 0;
 
 	/* assume 'not updated' by default */
 	Assert(filter);
@@ -514,17 +626,16 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 	/* we better be in the hashing phase */
 	Assert(BLOOM_IS_HASHED(filter));
 
-	/* compute the hashes, used for the bloom filter */
-	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
-
-	h = big_h % filter->nbits;
-	d = big_h % (filter->nbits - 1);
-
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		int byte = (h / 8);
-		int bit  = (h % 8);
+		int partlen = filter->partlens[i];
+		int bitloc = part_boundary + (value % partlen);
+
+		Assert(bitloc < filter->nbits);
+
+		int byte = (bitloc / 8);
+		int bit  = (bitloc % 8);
 
 		/* if the bit is not set, set it and remember we did that */
 		if (! (filter->data[byte] & (0x01 << bit)))
@@ -536,12 +647,7 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 		}
 
 		/* next bit */
-		h += d++;
-		if (h >= filter->nbits)
-			h -= filter->nbits;
-
-		if (d == filter->nbits)
-			d = 0;
+		part_boundary += partlen;
 	}
 
 	return filter;
@@ -571,6 +677,7 @@ bloom_switch_to_hashing(BloomFilter *filter)
 
 	newfilter->nhashes = filter->nhashes;
 	newfilter->nbits = filter->nbits;
+	memcpy(newfilter->partlens, filter->partlens, filter->nhashes * sizeof(uint16));
 	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
 
 	SET_VARSIZE(newfilter, len);
@@ -595,7 +702,7 @@ static bool
 bloom_contains_value(BloomFilter *filter, uint32 value)
 {
 	int		i;
-	uint32	big_h, h, d;
+	int		part_boundary = 0;
 
 	Assert(filter);
 
@@ -624,28 +731,23 @@ bloom_contains_value(BloomFilter *filter, uint32 value)
 	/* now the regular hashing mode */
 	Assert(BLOOM_IS_HASHED(filter));
 
-	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
-
-	h = big_h % filter->nbits;
-	d = big_h % (filter->nbits - 1);
-
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		int byte = (h / 8);
-		int bit  = (h % 8);
+		int partlen = filter->partlens[i];
+		int bitloc = part_boundary + (value % partlen);
+
+		Assert(bitloc < filter->nbits);
+
+		int byte = (bitloc / 8);
+		int bit  = (bitloc % 8);
 
 		/* if the bit is not set, the value is not there */
 		if (! (filter->data[byte] & (0x01 << bit)))
 			return false;
 
 		/* next bit */
-		h += d++;
-		if (h >= filter->nbits)
-			h -= filter->nbits;
-
-		if (d == filter->nbits)
-			d = 0;
+		part_boundary += partlen;
 	}
 
 	/* all hashes found in bloom filter */
@@ -1064,8 +1166,14 @@ brin_bloom_summary_out(PG_FUNCTION_ARGS)
 
 	if (BLOOM_IS_HASHED(filter))
 	{
-		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+		appendStringInfo(&str,
+						 "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u  partition lengths:  [",
 						 filter->nhashes, filter->nbits, filter->nbits_set);
+		for (int i = 0; i < filter->nhashes - 1; i++)
+		{
+			appendStringInfo(&str, "%u, ", filter->partlens[i]);
+		}
+		appendStringInfo(&str, "%u]", filter->partlens[filter->nhashes - 1]);
 	}
 	else
 	{
-- 
2.26.2

0006-BRIN-minmax-multi-indexes-20201103.patchtext/plain; charset=us-asciiDownload
From ff72079ced7ba5bfd101b852a6dd13cdcd582f39 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:49 +0200
Subject: [PATCH 6/7] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2389 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4792 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index fea13791d6..660769ef5e 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -590,6 +590,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..f9a7ae6608
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2389 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 46e6b23c87..8bdf837070 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -138,6 +138,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		for (datumno = 0;
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
@@ -398,6 +406,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -477,6 +490,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index a9ccc3995b..064a93d09b 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 2f1f144db4..d3d12a7b99 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 12033d48ec..cda1ad4fbe 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1900,6 +1900,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1977,6 +2123,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2003,6 +2166,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2070,6 +2249,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2101,6 +2346,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2123,6 +2385,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2424,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2209,6 +2505,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2360,6 +2673,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2415,6 +2874,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2437,6 +2913,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2493,6 +2986,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2515,6 +3025,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2581,6 +3108,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 15f3baea15..8d212ede8b 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -953,6 +953,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1048,6 +1194,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1094,6 +1257,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1144,6 +1324,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1197,6 +1451,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1231,6 +1505,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1264,6 +1558,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1349,6 +1663,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1474,6 +1807,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1544,6 +2041,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1578,6 +2095,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1638,6 +2175,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1669,7 +2226,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1724,6 +2302,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index ca747a03b9..27ef11b81b 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,6 +307,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -306,33 +318,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -348,36 +378,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -389,6 +437,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -398,6 +449,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -407,6 +461,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 8875079698..8e6d9eec16 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,26 +220,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -242,10 +262,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -254,12 +278,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c746efd33b..b0dd5f5040 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8129,6 +8129,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11033,3 +11098,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 59da6a9804..af8d010c69 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -628,3 +628,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 72c7598c89..f855633634 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4986,12 +4986,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 97bf9797de..55a30a6b47 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index a8e677dd0b..0e3cadd054 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 5776dbdd73..dc2a9ce36c 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0007-Ignore-correlation-for-new-BRIN-opclasses-20201103.patchtext/plain; charset=us-asciiDownload
From 6c6ad41abbcfcb37ba13f3df8b0e5dceacb845cc Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 7/7] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index b5f07954a6..769feb6b3d 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -783,6 +783,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index f9a7ae6608..4f6262f241 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1306,6 +1306,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index bec357fcef..d84bf4726c 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7350,7 +7351,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7372,6 +7374,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7405,6 +7408,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7475,6 +7489,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

#117Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#116)
10 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

Here's a rebased version of the patch series, to keep the cfbot happy.
I've also restricted the false positive rate to [0.001, 0.1] instead of
the original [0.0, 1.0], per discussion on this thread.

I've done a bunch of experiments, comparing the "regular" bloom indexes
with the one-hashing scheme proposed by John Naylor. I've been wondering
if there's some measurable difference, especially in:

* efficiency (query duration)

* false positive rate depending on the fill factor

So I ran a bunch of tests on synthetic data sets, varying parameters
affecting the BRIN bloom indexes:

1) different pages_per_range

2) number of distinct values per range

3) fill factor of the bloom filter (66%, 100%, 200%)

Attached is a script I used to test this, and a simple spreadsheet
summarizing the results, comparing the results for each combination of
parameters. For each combination it shows average query duration (over
10 runs) and scan fraction (what fraction of table was scanned).

Overall, I think there's very little difference, particularly in the
"match" cases when we're searching for a value that we know is in the
table. The one-hash variant seems to perform a bit better, but the
difference is fairly small.

In the "mismatch" cases (searching for value that is not in the table)
the differences are more significant, but it might be noise. It does
seem much more "green" than "red", i.e. the one-hash variant seems to be
faster (although this does depend on the values for formatting).

To sum this up, I think the one-hash approach seems interesting. It's
not going to give us huge speedups because we're only hashing int32
values anyway (not the source data), but it's worth exploring.

I've started looking at the one-hash code changes, and I've discovered a
couple issues. I've been wondering how expensive the naive prime sieve
is - it's not extremely hot code path, as we're only running it for each
page range. But still. So my plan was to create the largest bloom filter
possible, and see how much time generate_primes() takes.

So I initialized a cluster with 32kB blocks and tried to do this:

create index on t
using brin (a int4_bloom_ops(n_distinct_per_range=120000,
false_positive_rate=0.1));

which ends up using nbits=575104 (which is 2x the page size, but let's
ignore that) and nhashes=3. That however crashes and burns, because:

a) set_bloom_partitions does this:

while (primes[pidx + nhashes - 1] <= target && primes[pidx] > 0)
pidx++;

which is broken, because the second part of the condition only checks
the current index - we may end up using nhashes primes after that, and
some of them may be 0. So this needs to be:

while (primes[pidx + nhashes - 1] <= target &&
primes[pidx + nhashes] > 0)
pidx++;

(We know there's always at least one 0 at the end, so it's OK not to
check the length explicitly.)

b) set_bloom_partitions does this to generate primes:

/*
* Increase the limit to ensure we have some primes higher than
* the target partition length. The 100 value is arbitrary, but
* should be well over what we need.
*/
primes = generate_primes(target_partlen + 100);

It's not clear to me why 100 is sufficient, particularly for large page
sizes. AFAIK the primes get more and more sparse, so how does this
guarantee we'll get enough "sufficiently large" primes?

c) generate_primes uses uint16 to store the primes, so it can only
generate primes up to 32768. That's (probably) enough for 8kB pages, but
for 32kB pages it's clearly insufficient.

I've fixes these issues in a separate WIP patch, with some simple
debugging logging.

As for the original question how expensive this naive sieve is, I
haven't been able to measure any significant timings. The logging aroung
generate_primes usually looks like this:

2020-11-07 20:36:10.614 CET [232789] LOG: generating primes nbits
575104 nhashes 3 target_partlen 191701
2020-11-07 20:36:10.614 CET [232789] LOG: primes generated

So it takes 0.000 second for this extreme page size. I don't think we
need to invent anything more elaborate.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

i5.odsapplication/vnd.oasis.opendocument.spreadsheet; name=i5.odsDownload
0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20201107.patchtext/x-patch; charset=UTF-8; name=0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20201107.patchDownload
From 83eb4a6291d80e5240f775c3057f0ecb54795d1d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/8] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1f72562c60..ccf609e799 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +528,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,51 +538,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..853727190c 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +443,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..b233558310 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c01da4bf01..bcbbc97de0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8137,7 +8137,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8153,7 +8153,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20201107.patchtext/x-patch; charset=UTF-8; name=0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20201107.patchDownload
From 1e63b78e2c391363ba326d4fbcb5348b024854a2 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/8] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 239 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index ccf609e799..4dd29c7b10 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -389,8 +367,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +395,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,23 +422,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -463,9 +447,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,15 +541,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -694,7 +734,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -723,25 +762,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1520,6 +1542,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1573,3 +1628,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 853727190c..f5eaef416f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -574,37 +506,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index b233558310..8886ad984e 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0003-Optimize-allocations-in-bringetbitmap-20201107.patchtext/x-patch; charset=UTF-8; name=0003-Optimize-allocations-in-bringetbitmap-20201107.patchDownload
From 96e0faa23597a41d34a0cb66e7074dc1a64c3632 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/8] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 4dd29c7b10..470431010d 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -372,6 +372,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -397,11 +400,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -437,9 +479,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -450,17 +492,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0004-BRIN-bloom-indexes-20201107.patchtext/x-patch; charset=UTF-8; name=0004-BRIN-bloom-indexes-20201107.patchDownload
From a51aaac6de42b16078cfe549e60246ac51fda328 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sat, 7 Nov 2020 15:21:18 +0100
Subject: [PATCH 4/8] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    |  178 ++++
 doc/src/sgml/ref/create_index.sgml        |   31 +
 src/backend/access/brin/Makefile          |    1 +
 src/backend/access/brin/brin_bloom.c      | 1112 +++++++++++++++++++++
 src/include/access/brin.h                 |    2 +
 src/include/access/brin_internal.h        |    4 +
 src/include/catalog/pg_amop.dat           |  170 ++++
 src/include/catalog/pg_amproc.dat         |  447 +++++++++
 src/include/catalog/pg_opclass.dat        |   72 ++
 src/include/catalog/pg_opfamily.dat       |   38 +
 src/include/catalog/pg_proc.dat           |   34 +
 src/include/catalog/pg_type.dat           |    7 +
 src/test/regress/expected/brin_bloom.out  |  456 +++++++++
 src/test/regress/expected/opr_sanity.out  |    3 +-
 src/test/regress/expected/psql.out        |    3 +-
 src/test/regress/expected/type_sanity.out |    7 +-
 src/test/regress/parallel_schedule        |    5 +
 src/test/regress/serial_schedule          |    1 +
 src/test/regress/sql/brin_bloom.sql       |  404 ++++++++
 19 files changed, 2970 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 749db2845e..fea13791d6 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -559,6 +559,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..52b3d477c8
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,1112 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.001		/* 0.1% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.1			/* 10% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i, j, k;
+	Size	len;
+
+	uint32 *values;
+	uint32 *result;
+	uint32 *sorted;
+	uint32 *unsorted;
+
+	int		nvalues;
+	int		nsorted;
+	int		nunsorted;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* when already fully sorted, no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* result of merging */
+	k = 0;
+	result = (uint32 *) palloc(sizeof(uint32) * filter->nvalues);
+
+	/* first we have sorted data, then unsorted */
+	sorted = values;
+	nsorted = filter->nsorted;
+
+	unsorted = &values[filter->nsorted];
+	nunsorted = filter->nvalues - filter->nsorted;
+
+	/* sort the unsorted part, then merge the two parts */
+	pg_qsort(unsorted, nunsorted, sizeof(uint32), cmp_uint32);
+
+	i = 0;	/* sorted index */
+	j = 0;	/* unsorted index */
+
+	while ((i < nsorted) && (j < nunsorted))
+	{
+		if (sorted[i] <= unsorted[j])
+			result[k++] = sorted[i++];
+		else
+			result[k++] = unsorted[j++];
+	}
+
+	while (i < nsorted)
+		result[k++] = sorted[i++];
+
+	while (j < nunsorted)
+		result[k++] = unsorted[j++];
+
+	/* compact - remove duplicate values */
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (result[i] != result[nvalues - 1])
+			result[nvalues++] = result[i];
+	}
+
+	memcpy(filter->data, result, sizeof(uint32) * nvalues);
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index bbe357fbc0..3c0df5597f 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1689,6 +1689,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1706,6 +1711,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1723,6 +1733,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1869,6 +1884,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1886,6 +1939,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1903,6 +1961,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1920,6 +1983,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -1986,6 +2054,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2003,6 +2085,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2020,6 +2107,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2037,6 +2129,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2074,6 +2171,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2091,6 +2193,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2237,6 +2344,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2254,6 +2399,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2271,6 +2421,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2322,6 +2477,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2339,6 +2499,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2400,6 +2565,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index a8e0c4ff8a..15f3baea15 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -772,6 +772,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -785,6 +803,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -798,6 +834,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -899,6 +953,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -912,6 +1018,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -924,6 +1048,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -936,6 +1077,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -986,6 +1144,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1000,6 +1197,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1014,6 +1231,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1027,6 +1264,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1061,6 +1316,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1074,6 +1349,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1181,6 +1474,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1195,6 +1544,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1209,6 +1578,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1249,6 +1638,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1262,6 +1671,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1297,6 +1724,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..ca747a03b9 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +389,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..8875079698 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index bcbbc97de0..7351ecb07e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8160,6 +8160,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11006,3 +11026,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 21a467a7a7..59da6a9804 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -621,3 +621,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..19b866283a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0009)
+);
+ERROR:  value 0.0009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "0.100000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.11)
+);
+ERROR:  value 0.11 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "0.100000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 7ed29b4961..9b5715da86 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2018,6 +2018,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2081,7 +2082,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index daac0ff49d..72c7598c89 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4989,8 +4989,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ae89ed7f0b..69c39a1ca6 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 525bdc804f..6388bc7843 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..3c2ef56316
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.11)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0005-use-one-hash-bloom-variant-20201107.patchtext/x-patch; charset=UTF-8; name=0005-use-one-hash-bloom-variant-20201107.patchDownload
From 2055c7aa983ccd8d2993dd5219b90cb5f7314a37 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Mon, 2 Nov 2020 23:55:28 +0100
Subject: [PATCH 5/8] use one-hash bloom variant

---
 src/backend/access/brin/brin_bloom.c | 176 +++++++++++++++++++++------
 1 file changed, 142 insertions(+), 34 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 52b3d477c8..73f8478b4c 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -206,6 +206,9 @@ typedef struct BloomOptions
 #define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.1			/* 10% fp rate */
 #define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
 
+/* With the minimum allowed false positive rate of 0.001, we need up to 10 hashes */
+#define		BLOOM_MAX_NUM_PARTITIONS	10
+
 #define BloomGetNDistinctPerRange(opts) \
 	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
 	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
@@ -273,6 +276,7 @@ typedef struct BloomFilter
 	uint8	nhashes;	/* number of hash functions */
 	uint32	nbits;		/* number of bits in the bitmap (size) */
 	uint32	nbits_set;	/* number of bits set to 1 */
+	uint16	partlens[BLOOM_MAX_NUM_PARTITIONS];	/* partition lengths */
 
 	/* data of the bloom filter (used both for sorted and hashed phase) */
 	char	data[FLEXIBLE_ARRAY_MEMBER];
@@ -282,6 +286,114 @@ typedef struct BloomFilter
 static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
 
 
+/*
+ * generate_primes
+ * 		returns array of all primes less than limit
+ *
+ * WIP: very naive prime sieve; could be optimized using segmented ranges
+ */
+static uint16 *
+generate_primes(int limit)
+{
+	/* upper bound of number of primes below limit */
+	/* WIP: reference for this number */
+	int numprimes = 1.26 * limit / log(limit);
+
+	bool *is_composite = (bool *) palloc0(limit * sizeof(bool));
+	uint16 *primes = (uint16 *) palloc0(numprimes * sizeof(uint16));
+
+	int maxfactor = floor(sqrt(limit));
+	int factor = 2;	/* first prime */
+
+	/* mark the sieve where the index is composite */
+	while (factor < maxfactor)
+	{
+		for (int i = factor * factor; i < limit; i += factor)
+			 is_composite[i] = true;
+		do { factor++; } while (is_composite[factor]);
+	}
+
+	/* the unmarked numbers are prime, so copy over */
+	for (int i = 2, j = 0; i < limit && j < numprimes; i++)
+	{
+		if (!is_composite[i])
+			primes[j++] = i;
+	}
+
+	/* there should still be some zeroes at the end, but make sure */
+	primes[numprimes - 1] = 0;
+
+	/* pretty large, so free it now (segmented ranges would make it smaller) */
+	pfree(is_composite);
+	return primes;
+}
+
+/*
+ * set_bloom_partitions
+ * 		Calculate k moduli for one-hashing bloom filter.
+ *
+ * Find consecutive primes whose sum is close to nbits and
+ * return the sum. Copy the primes to the filter to use as
+ * partition lengths.
+ * WIP: one-hashing bf paper ref somewhere
+ */
+static uint32
+set_bloom_partitions(int nbits, uint8 nhashes, uint16 *partlens)
+{
+	int		min, diff, incr;
+	int		pidx = 0;
+	int		sum = 0;
+	int		target_partlen = nbits / nhashes;
+
+	/*
+	 * Increase the limit to ensure we have some primes higher than
+	 * the target partition length. The 100 value is arbitrary, but
+	 * should be well over what we need.
+	 */
+	uint16 *primes = generate_primes(target_partlen + 100);
+
+	/*
+	 * In our array of primes, find a sequence of length nhashes, whose
+	 * last item is close to our target partition length. The end of the
+	 * array will be filled with zeros, so we need to guard against that.
+	 */
+	while (primes[pidx + nhashes - 1] <= target_partlen &&
+		   primes[pidx] > 0)
+		pidx++;
+
+	for (int i = 0; i < nhashes; i++)
+		sum += primes[pidx + i];
+
+	/*
+	 * Since all the primes are less than or equal the desired partition
+	 * length, the sum is somewhat less than nbits. Increment the starting
+	 * point until we find the sequence of primes whose sum is closest to
+	 * nbits. It doesn't matter whether it's higher or lower.
+	 */
+	min = abs(nbits - sum);
+	for (;;)
+	{
+		incr = primes[pidx + nhashes] - primes[pidx];
+		diff = abs(nbits - (sum + incr));
+		if (diff >= min)
+			break;
+
+		min = diff;
+		sum += incr;
+		pidx++;
+	}
+
+	memcpy(partlens, &primes[pidx], nhashes * sizeof(uint16));
+
+	/* WIP: assuming it's not important to pfree primes */
+
+	/*
+	 * The actual filter length will be the sum of the partition lengths
+	 * rounded up to the nearest byte.
+	 */
+	return (uint32) ((sum + 7) / 8) * 8;
+}
+
 /*
  * bloom_init
  * 		Initialize the Bloom Filter, allocate all the memory.
@@ -304,15 +416,13 @@ bloom_init(int ndistinct, double false_positive_rate)
 
 	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
 
-	/* round m to whole bytes */
-	m = ((m + 7) / 8) * 8;
-
 	/*
 	 * round(log(2.0) * m / ndistinct), but assume round() may not be
 	 * available on Windows
 	 */
 	k = log(2.0) * m / ndistinct;
 	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+	k = Min(k, BLOOM_MAX_NUM_PARTITIONS);
 
 	/*
 	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
@@ -326,7 +436,9 @@ bloom_init(int ndistinct, double false_positive_rate)
 
 	filter->flags = 0;	/* implies SORTED phase */
 	filter->nhashes = (int) k;
-	filter->nbits = m;
+
+	/* calculate the partition lengths and adjust m to match */
+	filter->nbits = set_bloom_partitions(m, k, filter->partlens);
 
 	SET_VARSIZE(filter, len);
 
@@ -448,7 +560,7 @@ static BloomFilter *
 bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 {
 	int		i;
-	uint32	big_h, h, d;
+	int		part_boundary = 0;
 
 	/* assume 'not updated' by default */
 	Assert(filter);
@@ -517,17 +629,16 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 	/* we better be in the hashing phase */
 	Assert(BLOOM_IS_HASHED(filter));
 
-	/* compute the hashes, used for the bloom filter */
-	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
-
-	h = big_h % filter->nbits;
-	d = big_h % (filter->nbits - 1);
-
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		int byte = (h / 8);
-		int bit  = (h % 8);
+		int partlen = filter->partlens[i];
+		int bitloc = part_boundary + (value % partlen);
+
+		int byte = (bitloc / 8);
+		int bit  = (bitloc % 8);
+
+		Assert(bitloc < filter->nbits);
 
 		/* if the bit is not set, set it and remember we did that */
 		if (! (filter->data[byte] & (0x01 << bit)))
@@ -539,12 +650,7 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 		}
 
 		/* next bit */
-		h += d++;
-		if (h >= filter->nbits)
-			h -= filter->nbits;
-
-		if (d == filter->nbits)
-			d = 0;
+		part_boundary += partlen;
 	}
 
 	return filter;
@@ -574,6 +680,7 @@ bloom_switch_to_hashing(BloomFilter *filter)
 
 	newfilter->nhashes = filter->nhashes;
 	newfilter->nbits = filter->nbits;
+	memcpy(newfilter->partlens, filter->partlens, filter->nhashes * sizeof(uint16));
 	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
 
 	SET_VARSIZE(newfilter, len);
@@ -598,7 +705,7 @@ static bool
 bloom_contains_value(BloomFilter *filter, uint32 value)
 {
 	int		i;
-	uint32	big_h, h, d;
+	int		part_boundary = 0;
 
 	Assert(filter);
 
@@ -627,28 +734,23 @@ bloom_contains_value(BloomFilter *filter, uint32 value)
 	/* now the regular hashing mode */
 	Assert(BLOOM_IS_HASHED(filter));
 
-	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
-
-	h = big_h % filter->nbits;
-	d = big_h % (filter->nbits - 1);
-
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		int byte = (h / 8);
-		int bit  = (h % 8);
+		int partlen = filter->partlens[i];
+		int bitloc = part_boundary + (value % partlen);
+
+		int byte = (bitloc / 8);
+		int bit  = (bitloc % 8);
+
+		Assert(bitloc < filter->nbits);
 
 		/* if the bit is not set, the value is not there */
 		if (! (filter->data[byte] & (0x01 << bit)))
 			return false;
 
 		/* next bit */
-		h += d++;
-		if (h >= filter->nbits)
-			h -= filter->nbits;
-
-		if (d == filter->nbits)
-			d = 0;
+		part_boundary += partlen;
 	}
 
 	/* all hashes found in bloom filter */
@@ -1069,8 +1171,14 @@ brin_bloom_summary_out(PG_FUNCTION_ARGS)
 
 	if (BLOOM_IS_HASHED(filter))
 	{
-		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+		appendStringInfo(&str,
+						 "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u  partition lengths:  [",
 						 filter->nhashes, filter->nbits, filter->nbits_set);
+		for (int i = 0; i < filter->nhashes - 1; i++)
+		{
+			appendStringInfo(&str, "%u, ", filter->partlens[i]);
+		}
+		appendStringInfo(&str, "%u]", filter->partlens[filter->nhashes - 1]);
 	}
 	else
 	{
-- 
2.26.2

0006-one-hash-tweaks-20201107.patchtext/x-patch; charset=UTF-8; name=0006-one-hash-tweaks-20201107.patchDownload
From f217c117b3a759b11ac3665c368e6353d9b0f5d9 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sat, 7 Nov 2020 20:36:38 +0100
Subject: [PATCH 6/8] one-hash tweaks

---
 src/backend/access/brin/brin_bloom.c | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 73f8478b4c..f6e65c0157 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -292,7 +292,7 @@ static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
  *
  * WIP: very naive prime sieve; could be optimized using segmented ranges
  */
-static uint16 *
+static uint32 *
 generate_primes(int limit)
 {
 	/* upper bound of number of primes below limit */
@@ -300,7 +300,7 @@ generate_primes(int limit)
 	int numprimes = 1.26 * limit / log(limit);
 
 	bool *is_composite = (bool *) palloc0(limit * sizeof(bool));
-	uint16 *primes = (uint16 *) palloc0(numprimes * sizeof(uint16));
+	uint32 *primes = (uint32 *) palloc0(numprimes * sizeof(uint32));
 
 	int maxfactor = floor(sqrt(limit));
 	int factor = 2;	/* first prime */
@@ -344,13 +344,18 @@ set_bloom_partitions(int nbits, uint8 nhashes, uint16 *partlens)
 	int		pidx = 0;
 	int		sum = 0;
 	int		target_partlen = nbits / nhashes;
+	uint32 *primes;
+
+	elog(LOG, "generating primes nbits %u nhashes %u target_partlen %d", nbits, nhashes, target_partlen);
 
 	/*
 	 * Increase the limit to ensure we have some primes higher than
 	 * the target partition length. The 100 value is arbitrary, but
 	 * should be well over what we need.
 	 */
-	uint16 *primes = generate_primes(target_partlen + 100);
+	primes = generate_primes(target_partlen + 100);
+
+	elog(LOG, "primes generated");
 
 	/*
 	 * In our array of primes, find a sequence of length nhashes, whose
@@ -358,12 +363,14 @@ set_bloom_partitions(int nbits, uint8 nhashes, uint16 *partlens)
 	 * array will be filled with zeros, so we need to guard against that.
 	 */
 	while (primes[pidx + nhashes - 1] <= target_partlen &&
-		   primes[pidx] > 0)
+		   primes[pidx + nhashes] > 0)
 		pidx++;
 
 	for (int i = 0; i < nhashes; i++)
 		sum += primes[pidx + i];
 
+	elog(LOG, "nbits %d sum %d", nbits, sum);
+
 	/*
 	 * Since all the primes are less than or equal the desired partition
 	 * length, the sum is somewhat less than nbits. Increment the starting
@@ -383,6 +390,8 @@ set_bloom_partitions(int nbits, uint8 nhashes, uint16 *partlens)
 		pidx++;
 	}
 
+	elog(LOG, "nbits %d sum %d", nbits, sum);
+
 	memcpy(partlens, &primes[pidx], nhashes * sizeof(uint16));
 
 	/* WIP: assuming it's not important to pfree primes */
-- 
2.26.2

0007-BRIN-minmax-multi-indexes-20201107.patchtext/x-patch; charset=UTF-8; name=0007-BRIN-minmax-multi-indexes-20201107.patchDownload
From 2922c955903458bc49e4d8f7c4812e1949bdfc73 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sat, 7 Nov 2020 15:26:11 +0100
Subject: [PATCH 7/8] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2389 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4792 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index fea13791d6..660769ef5e 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -590,6 +590,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..f9a7ae6608
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2389 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 6774f597a4..91833f12c5 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -498,6 +506,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -577,6 +590,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index a9ccc3995b..064a93d09b 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 2f1f144db4..d3d12a7b99 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 3c0df5597f..64ae71a58d 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1884,6 +1884,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1961,6 +2107,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -1987,6 +2150,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2054,6 +2233,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2085,6 +2330,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2107,6 +2369,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2129,6 +2408,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2193,6 +2489,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2344,6 +2657,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2399,6 +2858,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2421,6 +2897,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2477,6 +2970,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2499,6 +3009,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2565,6 +3092,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 15f3baea15..8d212ede8b 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -953,6 +953,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1048,6 +1194,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1094,6 +1257,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1144,6 +1324,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1197,6 +1451,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1231,6 +1505,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1264,6 +1558,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1349,6 +1663,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1474,6 +1807,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1544,6 +2041,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1578,6 +2095,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1638,6 +2175,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1669,7 +2226,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1724,6 +2302,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index ca747a03b9..27ef11b81b 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,6 +307,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -306,33 +318,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -348,36 +378,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -389,6 +437,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -398,6 +449,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -407,6 +461,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 8875079698..8e6d9eec16 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,26 +220,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -242,10 +262,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -254,12 +278,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7351ecb07e..7a059dfc74 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8143,6 +8143,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11040,3 +11105,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 59da6a9804..af8d010c69 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -628,3 +628,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 72c7598c89..f855633634 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4986,12 +4986,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 97bf9797de..55a30a6b47 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 69c39a1ca6..5050980cd4 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 6388bc7843..aa8bd4437e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0008-Ignore-correlation-for-new-BRIN-opclasses-20201107.patchtext/x-patch; charset=UTF-8; name=0008-Ignore-correlation-for-new-BRIN-opclasses-20201107.patchDownload
From 3c1bcbb7f79bc651b9d3cbc752ae0c15dd0be730 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 8/8] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index f6e65c0157..4d0f01a3ea 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -795,6 +795,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index f9a7ae6608..4f6262f241 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1306,6 +1306,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index bec357fcef..d84bf4726c 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7350,7 +7351,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7372,6 +7374,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7405,6 +7408,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7475,6 +7489,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

bloom-test.shapplication/x-shellscript; name=bloom-test.shDownload
#118Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#117)
8 attachment(s)
Re: WIP: BRIN multi-range indexes

Seems I forgot to replace uint16 with uint32 in couple places when
fixing the one-hash code, so it was triggering SIGFPE because of
division by 0. Here's a fixed patch series.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20201108.patchtext/x-patch; charset=UTF-8; name=0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20201108.patchDownload
From 60240e7fdd1674f100cde863e4a97602bad79d52 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/8] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1f72562c60..ccf609e799 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +528,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,51 +538,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 7e380d66ed..853727190c 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -385,7 +443,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -406,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -425,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -460,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -470,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..b233558310 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c01da4bf01..bcbbc97de0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8137,7 +8137,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8153,7 +8153,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20201108.patchtext/x-patch; charset=UTF-8; name=0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20201108.patchDownload
From e4f9a4c5a19a976552ff67472cc6504d7c5c3ed1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/8] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 239 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index ccf609e799..4dd29c7b10 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -389,8 +367,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +395,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,23 +422,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -463,9 +447,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,15 +541,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -694,7 +734,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -723,25 +762,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1520,6 +1542,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1573,3 +1628,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 853727190c..f5eaef416f 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -574,37 +506,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index b233558310..8886ad984e 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0003-Optimize-allocations-in-bringetbitmap-20201108.patchtext/x-patch; charset=UTF-8; name=0003-Optimize-allocations-in-bringetbitmap-20201108.patchDownload
From 17a3fc30c21ea02cf5bc87b303336ea1a9b529e0 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/8] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 4dd29c7b10..470431010d 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -372,6 +372,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -397,11 +400,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -437,9 +479,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -450,17 +492,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0004-BRIN-bloom-indexes-20201108.patchtext/x-patch; charset=UTF-8; name=0004-BRIN-bloom-indexes-20201108.patchDownload
From 634698bf07f5e8aaeb8ad925e95cea049f089e5d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sat, 7 Nov 2020 15:21:18 +0100
Subject: [PATCH 4/8] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    |  178 ++++
 doc/src/sgml/ref/create_index.sgml        |   31 +
 src/backend/access/brin/Makefile          |    1 +
 src/backend/access/brin/brin_bloom.c      | 1112 +++++++++++++++++++++
 src/include/access/brin.h                 |    2 +
 src/include/access/brin_internal.h        |    4 +
 src/include/catalog/pg_amop.dat           |  170 ++++
 src/include/catalog/pg_amproc.dat         |  447 +++++++++
 src/include/catalog/pg_opclass.dat        |   72 ++
 src/include/catalog/pg_opfamily.dat       |   38 +
 src/include/catalog/pg_proc.dat           |   34 +
 src/include/catalog/pg_type.dat           |    7 +
 src/test/regress/expected/brin_bloom.out  |  456 +++++++++
 src/test/regress/expected/opr_sanity.out  |    3 +-
 src/test/regress/expected/psql.out        |    3 +-
 src/test/regress/expected/type_sanity.out |    7 +-
 src/test/regress/parallel_schedule        |    5 +
 src/test/regress/serial_schedule          |    1 +
 src/test/regress/sql/brin_bloom.sql       |  404 ++++++++
 19 files changed, 2970 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 749db2845e..fea13791d6 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -559,6 +559,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..52b3d477c8
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,1112 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.001		/* 0.1% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.1			/* 10% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i, j, k;
+	Size	len;
+
+	uint32 *values;
+	uint32 *result;
+	uint32 *sorted;
+	uint32 *unsorted;
+
+	int		nvalues;
+	int		nsorted;
+	int		nunsorted;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* when already fully sorted, no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* result of merging */
+	k = 0;
+	result = (uint32 *) palloc(sizeof(uint32) * filter->nvalues);
+
+	/* first we have sorted data, then unsorted */
+	sorted = values;
+	nsorted = filter->nsorted;
+
+	unsorted = &values[filter->nsorted];
+	nunsorted = filter->nvalues - filter->nsorted;
+
+	/* sort the unsorted part, then merge the two parts */
+	pg_qsort(unsorted, nunsorted, sizeof(uint32), cmp_uint32);
+
+	i = 0;	/* sorted index */
+	j = 0;	/* unsorted index */
+
+	while ((i < nsorted) && (j < nunsorted))
+	{
+		if (sorted[i] <= unsorted[j])
+			result[k++] = sorted[i++];
+		else
+			result[k++] = unsorted[j++];
+	}
+
+	while (i < nsorted)
+		result[k++] = sorted[i++];
+
+	while (j < nunsorted)
+		result[k++] = unsorted[j++];
+
+	/* compact - remove duplicate values */
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (result[i] != result[nvalues - 1])
+			result[nvalues++] = result[i];
+	}
+
+	memcpy(filter->data, result, sizeof(uint32) * nvalues);
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index bbe357fbc0..3c0df5597f 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1689,6 +1689,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1706,6 +1711,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1723,6 +1733,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1869,6 +1884,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1886,6 +1939,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1903,6 +1961,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1920,6 +1983,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -1986,6 +2054,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2003,6 +2085,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2020,6 +2107,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2037,6 +2129,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2074,6 +2171,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2091,6 +2193,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2237,6 +2344,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2254,6 +2399,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2271,6 +2421,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2322,6 +2477,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2339,6 +2499,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2400,6 +2565,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index a8e0c4ff8a..15f3baea15 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -772,6 +772,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -785,6 +803,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -798,6 +834,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -899,6 +953,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -912,6 +1018,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -924,6 +1048,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -936,6 +1077,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -986,6 +1144,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1000,6 +1197,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1014,6 +1231,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1027,6 +1264,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1061,6 +1316,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1074,6 +1349,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1181,6 +1474,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1195,6 +1544,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1209,6 +1578,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1249,6 +1638,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1262,6 +1671,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1297,6 +1724,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index f2342bb328..ca747a03b9 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -257,67 +257,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -326,18 +389,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index cf0fb325b3..8875079698 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,50 +180,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index bcbbc97de0..7351ecb07e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8160,6 +8160,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11006,3 +11026,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 21a467a7a7..59da6a9804 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -621,3 +621,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..19b866283a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0009)
+);
+ERROR:  value 0.0009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "0.100000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.11)
+);
+ERROR:  value 0.11 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "0.100000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 7ed29b4961..9b5715da86 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2018,6 +2018,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2081,7 +2082,7 @@ ORDER BY 1, 2, 3;
        4000 |           26 | >>
        4000 |           27 | >>=
        4000 |           28 | ^@
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index daac0ff49d..72c7598c89 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4989,8 +4989,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 274130e706..97bf9797de 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ae89ed7f0b..69c39a1ca6 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 525bdc804f..6388bc7843 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..3c2ef56316
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.11)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0005-use-one-hash-bloom-variant-20201108.patchtext/x-patch; charset=UTF-8; name=0005-use-one-hash-bloom-variant-20201108.patchDownload
From 566e9f01f86d702ca83b4152f4d0859c1a9ee9f1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Mon, 2 Nov 2020 23:55:28 +0100
Subject: [PATCH 5/8] use one-hash bloom variant

---
 src/backend/access/brin/brin_bloom.c | 176 +++++++++++++++++++++------
 1 file changed, 142 insertions(+), 34 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 52b3d477c8..73f8478b4c 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -206,6 +206,9 @@ typedef struct BloomOptions
 #define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.1			/* 10% fp rate */
 #define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
 
+/* With the minimum allowed false positive rate of 0.001, we need up to 10 hashes */
+#define		BLOOM_MAX_NUM_PARTITIONS	10
+
 #define BloomGetNDistinctPerRange(opts) \
 	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
 	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
@@ -273,6 +276,7 @@ typedef struct BloomFilter
 	uint8	nhashes;	/* number of hash functions */
 	uint32	nbits;		/* number of bits in the bitmap (size) */
 	uint32	nbits_set;	/* number of bits set to 1 */
+	uint16	partlens[BLOOM_MAX_NUM_PARTITIONS];	/* partition lengths */
 
 	/* data of the bloom filter (used both for sorted and hashed phase) */
 	char	data[FLEXIBLE_ARRAY_MEMBER];
@@ -282,6 +286,114 @@ typedef struct BloomFilter
 static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
 
 
+/*
+ * generate_primes
+ * 		returns array of all primes less than limit
+ *
+ * WIP: very naive prime sieve; could be optimized using segmented ranges
+ */
+static uint16 *
+generate_primes(int limit)
+{
+	/* upper bound of number of primes below limit */
+	/* WIP: reference for this number */
+	int numprimes = 1.26 * limit / log(limit);
+
+	bool *is_composite = (bool *) palloc0(limit * sizeof(bool));
+	uint16 *primes = (uint16 *) palloc0(numprimes * sizeof(uint16));
+
+	int maxfactor = floor(sqrt(limit));
+	int factor = 2;	/* first prime */
+
+	/* mark the sieve where the index is composite */
+	while (factor < maxfactor)
+	{
+		for (int i = factor * factor; i < limit; i += factor)
+			 is_composite[i] = true;
+		do { factor++; } while (is_composite[factor]);
+	}
+
+	/* the unmarked numbers are prime, so copy over */
+	for (int i = 2, j = 0; i < limit && j < numprimes; i++)
+	{
+		if (!is_composite[i])
+			primes[j++] = i;
+	}
+
+	/* there should still be some zeroes at the end, but make sure */
+	primes[numprimes - 1] = 0;
+
+	/* pretty large, so free it now (segmented ranges would make it smaller) */
+	pfree(is_composite);
+	return primes;
+}
+
+/*
+ * set_bloom_partitions
+ * 		Calculate k moduli for one-hashing bloom filter.
+ *
+ * Find consecutive primes whose sum is close to nbits and
+ * return the sum. Copy the primes to the filter to use as
+ * partition lengths.
+ * WIP: one-hashing bf paper ref somewhere
+ */
+static uint32
+set_bloom_partitions(int nbits, uint8 nhashes, uint16 *partlens)
+{
+	int		min, diff, incr;
+	int		pidx = 0;
+	int		sum = 0;
+	int		target_partlen = nbits / nhashes;
+
+	/*
+	 * Increase the limit to ensure we have some primes higher than
+	 * the target partition length. The 100 value is arbitrary, but
+	 * should be well over what we need.
+	 */
+	uint16 *primes = generate_primes(target_partlen + 100);
+
+	/*
+	 * In our array of primes, find a sequence of length nhashes, whose
+	 * last item is close to our target partition length. The end of the
+	 * array will be filled with zeros, so we need to guard against that.
+	 */
+	while (primes[pidx + nhashes - 1] <= target_partlen &&
+		   primes[pidx] > 0)
+		pidx++;
+
+	for (int i = 0; i < nhashes; i++)
+		sum += primes[pidx + i];
+
+	/*
+	 * Since all the primes are less than or equal the desired partition
+	 * length, the sum is somewhat less than nbits. Increment the starting
+	 * point until we find the sequence of primes whose sum is closest to
+	 * nbits. It doesn't matter whether it's higher or lower.
+	 */
+	min = abs(nbits - sum);
+	for (;;)
+	{
+		incr = primes[pidx + nhashes] - primes[pidx];
+		diff = abs(nbits - (sum + incr));
+		if (diff >= min)
+			break;
+
+		min = diff;
+		sum += incr;
+		pidx++;
+	}
+
+	memcpy(partlens, &primes[pidx], nhashes * sizeof(uint16));
+
+	/* WIP: assuming it's not important to pfree primes */
+
+	/*
+	 * The actual filter length will be the sum of the partition lengths
+	 * rounded up to the nearest byte.
+	 */
+	return (uint32) ((sum + 7) / 8) * 8;
+}
+
 /*
  * bloom_init
  * 		Initialize the Bloom Filter, allocate all the memory.
@@ -304,15 +416,13 @@ bloom_init(int ndistinct, double false_positive_rate)
 
 	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
 
-	/* round m to whole bytes */
-	m = ((m + 7) / 8) * 8;
-
 	/*
 	 * round(log(2.0) * m / ndistinct), but assume round() may not be
 	 * available on Windows
 	 */
 	k = log(2.0) * m / ndistinct;
 	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+	k = Min(k, BLOOM_MAX_NUM_PARTITIONS);
 
 	/*
 	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
@@ -326,7 +436,9 @@ bloom_init(int ndistinct, double false_positive_rate)
 
 	filter->flags = 0;	/* implies SORTED phase */
 	filter->nhashes = (int) k;
-	filter->nbits = m;
+
+	/* calculate the partition lengths and adjust m to match */
+	filter->nbits = set_bloom_partitions(m, k, filter->partlens);
 
 	SET_VARSIZE(filter, len);
 
@@ -448,7 +560,7 @@ static BloomFilter *
 bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 {
 	int		i;
-	uint32	big_h, h, d;
+	int		part_boundary = 0;
 
 	/* assume 'not updated' by default */
 	Assert(filter);
@@ -517,17 +629,16 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 	/* we better be in the hashing phase */
 	Assert(BLOOM_IS_HASHED(filter));
 
-	/* compute the hashes, used for the bloom filter */
-	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
-
-	h = big_h % filter->nbits;
-	d = big_h % (filter->nbits - 1);
-
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		int byte = (h / 8);
-		int bit  = (h % 8);
+		int partlen = filter->partlens[i];
+		int bitloc = part_boundary + (value % partlen);
+
+		int byte = (bitloc / 8);
+		int bit  = (bitloc % 8);
+
+		Assert(bitloc < filter->nbits);
 
 		/* if the bit is not set, set it and remember we did that */
 		if (! (filter->data[byte] & (0x01 << bit)))
@@ -539,12 +650,7 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 		}
 
 		/* next bit */
-		h += d++;
-		if (h >= filter->nbits)
-			h -= filter->nbits;
-
-		if (d == filter->nbits)
-			d = 0;
+		part_boundary += partlen;
 	}
 
 	return filter;
@@ -574,6 +680,7 @@ bloom_switch_to_hashing(BloomFilter *filter)
 
 	newfilter->nhashes = filter->nhashes;
 	newfilter->nbits = filter->nbits;
+	memcpy(newfilter->partlens, filter->partlens, filter->nhashes * sizeof(uint16));
 	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
 
 	SET_VARSIZE(newfilter, len);
@@ -598,7 +705,7 @@ static bool
 bloom_contains_value(BloomFilter *filter, uint32 value)
 {
 	int		i;
-	uint32	big_h, h, d;
+	int		part_boundary = 0;
 
 	Assert(filter);
 
@@ -627,28 +734,23 @@ bloom_contains_value(BloomFilter *filter, uint32 value)
 	/* now the regular hashing mode */
 	Assert(BLOOM_IS_HASHED(filter));
 
-	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
-
-	h = big_h % filter->nbits;
-	d = big_h % (filter->nbits - 1);
-
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		int byte = (h / 8);
-		int bit  = (h % 8);
+		int partlen = filter->partlens[i];
+		int bitloc = part_boundary + (value % partlen);
+
+		int byte = (bitloc / 8);
+		int bit  = (bitloc % 8);
+
+		Assert(bitloc < filter->nbits);
 
 		/* if the bit is not set, the value is not there */
 		if (! (filter->data[byte] & (0x01 << bit)))
 			return false;
 
 		/* next bit */
-		h += d++;
-		if (h >= filter->nbits)
-			h -= filter->nbits;
-
-		if (d == filter->nbits)
-			d = 0;
+		part_boundary += partlen;
 	}
 
 	/* all hashes found in bloom filter */
@@ -1069,8 +1171,14 @@ brin_bloom_summary_out(PG_FUNCTION_ARGS)
 
 	if (BLOOM_IS_HASHED(filter))
 	{
-		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+		appendStringInfo(&str,
+						 "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u  partition lengths:  [",
 						 filter->nhashes, filter->nbits, filter->nbits_set);
+		for (int i = 0; i < filter->nhashes - 1; i++)
+		{
+			appendStringInfo(&str, "%u, ", filter->partlens[i]);
+		}
+		appendStringInfo(&str, "%u]", filter->partlens[filter->nhashes - 1]);
 	}
 	else
 	{
-- 
2.26.2

0006-one-hash-tweaks-20201108.patchtext/x-patch; charset=UTF-8; name=0006-one-hash-tweaks-20201108.patchDownload
From b9837275d0f4abe614826b82134f58df2f783249 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sat, 7 Nov 2020 20:36:38 +0100
Subject: [PATCH 6/8] one-hash tweaks

---
 src/backend/access/brin/brin_bloom.c | 25 +++++++++++++++++--------
 1 file changed, 17 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 73f8478b4c..d5581abf7b 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -276,7 +276,7 @@ typedef struct BloomFilter
 	uint8	nhashes;	/* number of hash functions */
 	uint32	nbits;		/* number of bits in the bitmap (size) */
 	uint32	nbits_set;	/* number of bits set to 1 */
-	uint16	partlens[BLOOM_MAX_NUM_PARTITIONS];	/* partition lengths */
+	uint32	partlens[BLOOM_MAX_NUM_PARTITIONS];	/* partition lengths */
 
 	/* data of the bloom filter (used both for sorted and hashed phase) */
 	char	data[FLEXIBLE_ARRAY_MEMBER];
@@ -292,7 +292,7 @@ static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
  *
  * WIP: very naive prime sieve; could be optimized using segmented ranges
  */
-static uint16 *
+static uint32 *
 generate_primes(int limit)
 {
 	/* upper bound of number of primes below limit */
@@ -300,7 +300,7 @@ generate_primes(int limit)
 	int numprimes = 1.26 * limit / log(limit);
 
 	bool *is_composite = (bool *) palloc0(limit * sizeof(bool));
-	uint16 *primes = (uint16 *) palloc0(numprimes * sizeof(uint16));
+	uint32 *primes = (uint32 *) palloc0(numprimes * sizeof(uint32));
 
 	int maxfactor = floor(sqrt(limit));
 	int factor = 2;	/* first prime */
@@ -338,19 +338,24 @@ generate_primes(int limit)
  * WIP: one-hashing bf paper ref somewhere
  */
 static uint32
-set_bloom_partitions(int nbits, uint8 nhashes, uint16 *partlens)
+set_bloom_partitions(int nbits, uint8 nhashes, uint32 *partlens)
 {
 	int		min, diff, incr;
 	int		pidx = 0;
 	int		sum = 0;
 	int		target_partlen = nbits / nhashes;
+	uint32 *primes;
+
+	elog(LOG, "generating primes nbits %u nhashes %u target_partlen %d", nbits, nhashes, target_partlen);
 
 	/*
 	 * Increase the limit to ensure we have some primes higher than
 	 * the target partition length. The 100 value is arbitrary, but
 	 * should be well over what we need.
 	 */
-	uint16 *primes = generate_primes(target_partlen + 100);
+	primes = generate_primes(target_partlen + 100);
+
+	elog(LOG, "primes generated");
 
 	/*
 	 * In our array of primes, find a sequence of length nhashes, whose
@@ -358,12 +363,14 @@ set_bloom_partitions(int nbits, uint8 nhashes, uint16 *partlens)
 	 * array will be filled with zeros, so we need to guard against that.
 	 */
 	while (primes[pidx + nhashes - 1] <= target_partlen &&
-		   primes[pidx] > 0)
+		   primes[pidx + nhashes] > 0)
 		pidx++;
 
 	for (int i = 0; i < nhashes; i++)
 		sum += primes[pidx + i];
 
+	elog(LOG, "nbits %d sum %d", nbits, sum);
+
 	/*
 	 * Since all the primes are less than or equal the desired partition
 	 * length, the sum is somewhat less than nbits. Increment the starting
@@ -383,7 +390,9 @@ set_bloom_partitions(int nbits, uint8 nhashes, uint16 *partlens)
 		pidx++;
 	}
 
-	memcpy(partlens, &primes[pidx], nhashes * sizeof(uint16));
+	elog(LOG, "nbits %d sum %d", nbits, sum);
+
+	memcpy(partlens, &primes[pidx], nhashes * sizeof(uint32));
 
 	/* WIP: assuming it's not important to pfree primes */
 
@@ -680,7 +689,7 @@ bloom_switch_to_hashing(BloomFilter *filter)
 
 	newfilter->nhashes = filter->nhashes;
 	newfilter->nbits = filter->nbits;
-	memcpy(newfilter->partlens, filter->partlens, filter->nhashes * sizeof(uint16));
+	memcpy(newfilter->partlens, filter->partlens, filter->nhashes * sizeof(uint32));
 	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
 
 	SET_VARSIZE(newfilter, len);
-- 
2.26.2

0007-BRIN-minmax-multi-indexes-20201108.patchtext/x-patch; charset=UTF-8; name=0007-BRIN-minmax-multi-indexes-20201108.patchDownload
From 08de9ef122d15435ffaf78a90826f0b2d29c3871 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sat, 7 Nov 2020 15:26:11 +0100
Subject: [PATCH 7/8] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2389 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4792 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index fea13791d6..660769ef5e 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -590,6 +590,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..f9a7ae6608
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2389 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 6774f597a4..91833f12c5 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -498,6 +506,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -577,6 +590,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index a9ccc3995b..064a93d09b 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 2f1f144db4..d3d12a7b99 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 3c0df5597f..64ae71a58d 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1884,6 +1884,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1961,6 +2107,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -1987,6 +2150,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2054,6 +2233,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2085,6 +2330,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2107,6 +2369,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2129,6 +2408,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2193,6 +2489,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2344,6 +2657,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2399,6 +2858,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2421,6 +2897,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2477,6 +2970,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2499,6 +3009,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2565,6 +3092,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 15f3baea15..8d212ede8b 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -953,6 +953,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1048,6 +1194,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1094,6 +1257,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1144,6 +1324,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1197,6 +1451,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1231,6 +1505,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1264,6 +1558,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1349,6 +1663,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1474,6 +1807,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1544,6 +2041,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1578,6 +2095,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1638,6 +2175,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1669,7 +2226,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1724,6 +2302,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index ca747a03b9..27ef11b81b 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,18 +275,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -298,6 +307,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -306,33 +318,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -348,36 +378,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -389,6 +437,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -398,6 +449,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -407,6 +461,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 8875079698..8e6d9eec16 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -180,10 +180,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -192,10 +196,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -212,26 +220,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -242,10 +262,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -254,12 +278,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7351ecb07e..7a059dfc74 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8143,6 +8143,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11040,3 +11105,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 59da6a9804..af8d010c69 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -628,3 +628,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 72c7598c89..f855633634 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4986,12 +4986,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 97bf9797de..55a30a6b47 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('c','d','p') AND p1.typname NOT LIKE E'\\_%'
     (SELECT 1 FROM pg_type as p2
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid);
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a varlena array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 69c39a1ca6..5050980cd4 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 6388bc7843..aa8bd4437e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0008-Ignore-correlation-for-new-BRIN-opclasses-20201108.patchtext/x-patch; charset=UTF-8; name=0008-Ignore-correlation-for-new-BRIN-opclasses-20201108.patchDownload
From dabbb0c7297b99ecee66275922fd84e2a163cb26 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 8/8] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index d5581abf7b..06dc606cbd 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -795,6 +795,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index f9a7ae6608..4f6262f241 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1306,6 +1306,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index bec357fcef..d84bf4726c 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7350,7 +7351,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7372,6 +7374,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7405,6 +7408,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7475,6 +7489,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

#119John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#117)
Re: WIP: BRIN multi-range indexes

On Sat, Nov 7, 2020 at 4:38 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

Overall, I think there's very little difference, particularly in the
"match" cases when we're searching for a value that we know is in the
table. The one-hash variant seems to perform a bit better, but the
difference is fairly small.

In the "mismatch" cases (searching for value that is not in the table)
the differences are more significant, but it might be noise. It does
seem much more "green" than "red", i.e. the one-hash variant seems to be
faster (although this does depend on the values for formatting).

To sum this up, I think the one-hash approach seems interesting. It's
not going to give us huge speedups because we're only hashing int32
values anyway (not the source data), but it's worth exploring.

Thanks for testing! It seems you tested against the version with two
moduli, and not the alternative discussed in

/messages/by-id/20200918222702.omsieaphfj3ctqg3@development

which would in fact be rehashing the 32 bit values. I think that would be
the way to go if we don't use the one-hashing approach.

a) set_bloom_partitions does this:

while (primes[pidx + nhashes - 1] <= target && primes[pidx] > 0)
pidx++;

which is broken, because the second part of the condition only checks
the current index - we may end up using nhashes primes after that, and
some of them may be 0. So this needs to be:

while (primes[pidx + nhashes - 1] <= target &&
primes[pidx + nhashes] > 0)
pidx++;

Good catch.

b) set_bloom_partitions does this to generate primes:

/*
* Increase the limit to ensure we have some primes higher than
* the target partition length. The 100 value is arbitrary, but
* should be well over what we need.
*/
primes = generate_primes(target_partlen + 100);

It's not clear to me why 100 is sufficient, particularly for large page
sizes. AFAIK the primes get more and more sparse, so how does this
guarantee we'll get enough "sufficiently large" primes?

This value is not rigorous and should be improved, but I started with that
by looking at the table in section 3 in

https://primes.utm.edu/notes/gaps.html

I see two ways to make a stronger guarantee:

1. Take the average gap between primes near n, which is log(n), and
multiply that by BLOOM_MAX_NUM_PARTITIONS. Adding that to the target seems
a better heuristic than a constant, and is still simple to calculate.

With the pathological example you gave of n=575104, k=3 (target_partlen =
191701), the number to add is log(191701) * 10 = 122. By the table
referenced above, the largest prime gap under 360653 is 95, so we're
guaranteed to find at least one prime in the space of 122 above the target.
That will likely be enough to find the closest-to-target filter size for
k=3. Even if it weren't, nbits is so large that the relative difference is
tiny. I'd say a heuristic like this is most likely to be off precisely when
it matters the least. At this size, even if we find zero primes above our
target, the relative filter size is close to

(575104 - 3 * 95) / 575104 = 0.9995

For a more realistic bad-case target partition length, log(1327) * 10 = 72.
There are 33 composites after 1327, the largest such gap below 9551. That
would give five primes larger than the target
1361 1367 1373 1381 1399

which is more than enough for k<=10:

1297 + 1301 + 1303 + 1307 + 1319 + 1321 + 1327 + 1361 + 1367 + 1373
= 13276

2. Use a "segmented range" algorithm for the sieve and iterate until we get
k*2 primes, half below and half above the target. This would be an absolute
guarantee, but also more code, so I'm inclined against that.

c) generate_primes uses uint16 to store the primes, so it can only
generate primes up to 32768. That's (probably) enough for 8kB pages, but
for 32kB pages it's clearly insufficient.

Okay.

As for the original question how expensive this naive sieve is, I
haven't been able to measure any significant timings. The logging aroung
generate_primes usually looks like this:

2020-11-07 20:36:10.614 CET [232789] LOG: generating primes nbits
575104 nhashes 3 target_partlen 191701
2020-11-07 20:36:10.614 CET [232789] LOG: primes generated

So it takes 0.000 second for this extreme page size. I don't think we
need to invent anything more elaborate.

Okay, good to know. If we were concerned about memory, we could have it
check only odd numbers. That's a common feature of sieves, but also makes
the code a bit harder to understand if you haven't seen it before.

Also to fill in something I left for later, the reference for this

/* upper bound of number of primes below limit */
/* WIP: reference for this number */
int numprimes = 1.26 * limit / log(limit);

is

Rosser, J. Barkley; Schoenfeld, Lowell (1962). "Approximate formulas for
some functions of prime numbers". Illinois J. Math. 6: 64–94.
doi:10.1215/ijm/1255631807

More precisely, it's 30*log(113)/113 rounded up.

--
John Naylor
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#120Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#119)
Re: WIP: BRIN multi-range indexes

On 11/9/20 3:29 PM, John Naylor wrote:

On Sat, Nov 7, 2020 at 4:38 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

Overall, I think there's very little difference, particularly in the
"match" cases when we're searching for a value that we know is in the
table. The one-hash variant seems to perform a bit better, but the
difference is fairly small.

In the "mismatch" cases (searching for value that is not in the table)
the differences are more significant, but it might be noise. It does
seem much more "green" than "red", i.e. the one-hash variant seems to be
faster (although this does depend on the values for formatting).

To sum this up, I think the one-hash approach seems interesting. It's
not going to give us huge speedups because we're only hashing int32
values anyway (not the source data), but it's worth exploring.

Thanks for testing! It seems you tested against the version with two
moduli, and not the alternative discussed in

/messages/by-id/20200918222702.omsieaphfj3ctqg3@development
</messages/by-id/20200918222702.omsieaphfj3ctqg3@development&gt;

which would in fact be rehashing the 32 bit values. I think that would
be the way to go if we don't use the one-hashing approach.

Yeah. I forgot about this detail, and I may try again with the two-hash
variant, but I wonder how much difference would it make, considering the
results match the expected results (that is, the scan fraction" results
for fill_factor=100 match the target fpr almost perfectly).

I think there's a possibly-more important omission in the testing - I
forgot about the "sort mode" used initially, when the filter keeps the
actual hash values and only switches to hashing later. I wonder if that
plays role for some of the cases.

I'll investigate this a bit in the next round of tests.

a) set_bloom_partitions does this:

    while (primes[pidx + nhashes - 1] <= target && primes[pidx] > 0)
       pidx++;

which is broken, because the second part of the condition only checks
the current index - we may end up using nhashes primes after that, and
some of them may be 0. So this needs to be:

    while (primes[pidx + nhashes - 1] <= target &&
           primes[pidx + nhashes] > 0)
       pidx++;

Good catch.

b) set_bloom_partitions does this to generate primes:

    /*
     * Increase the limit to ensure we have some primes higher than
     * the target partition length. The 100 value is arbitrary, but
     * should be well over what we need.
     */
    primes = generate_primes(target_partlen + 100);

It's not clear to me why 100 is sufficient, particularly for large page
sizes. AFAIK the primes get more and more sparse, so how does this
guarantee we'll get enough "sufficiently large" primes?

This value is not rigorous and should be improved, but I started with
that by looking at the table in section 3 in

https://primes.utm.edu/notes/gaps.html
<https://primes.utm.edu/notes/gaps.html&gt;

I see two ways to make a stronger guarantee:

1. Take the average gap between primes near n, which is log(n), and
multiply that by BLOOM_MAX_NUM_PARTITIONS. Adding that to the target
seems a better heuristic than a constant, and is still simple to calculate.

With the pathological example you gave of n=575104, k=3 (target_partlen
= 191701), the number to add is log(191701) * 10 = 122.  By the table
referenced above, the largest prime gap under 360653 is 95, so we're
guaranteed to find at least one prime in the space of 122 above the
target. That will likely be enough to find the closest-to-target filter
size for k=3. Even if it weren't, nbits is so large that the relative
difference is tiny. I'd say a heuristic like this is most likely to be
off precisely when it matters the least. At this size, even if we find
zero primes above our target, the relative filter size is close to 

(575104 - 3 * 95) / 575104 = 0.9995

For a more realistic bad-case target partition length, log(1327) * 10 =
72. There are 33 composites after 1327, the largest such gap below 9551.
That would give five primes larger than the target
1361   1367   1373   1381   1399

which is more than enough for k<=10:

1297 +  1301  + 1303  + 1307  + 1319  + 1321  + 1327  + 1361 + 1367 +
1373 = 13276

2. Use a "segmented range" algorithm for the sieve and iterate until we
get k*2 primes, half below and half above the target. This would be an
absolute guarantee, but also more code, so I'm inclined against that.

Thanks, that makes sense.

While investigating the failures, I've tried increasing the values a
lot, without observing any measurable increase in runtime. IIRC I've
even used (10 * target_partlen) or something like that. That tells me
it's not very sensitive part of the code, so I'd suggest to simply use
something that we know is large enough to be safe.

For example, the largest bloom filter we can have is 32kB, i.e. 262kb,
at which point the largest gap is less than 95 (per the gap table). And
we may use up to BLOOM_MAX_NUM_PARTITIONS, so let's just use

BLOOM_MAX_NUM_PARTITIONS * 100

on the basis that we may need BLOOM_MAX_NUM_PARTITIONS partitions
before/after the target. We could consider the actual target being lower
(essentially 1/npartions of the nbits) which decreases the maximum gap,
but I don't think that's the extra complexity here.

FWIW I wonder if we should do something about bloom filters that we know
can get larger than page size. In the example I used, we know that
nbits=575104 is larger than page, so as the filter gets more full (and
thus more random and less compressible) it won't possibly fit. Maybe we
should reject that right away, instead of "delaying it" until later, on
the basis that it's easier to fix at CREATE INDEX time (compared to when
inserts/updates start failing at a random time).

The problem with this is of course that if the index is multi-column,
this may not be strict enough (i.e. each filter would fit independently,
but the whole index row is too large). But it's probably better to do at
least something, and maybe improve that later with some whole-row check.

c) generate_primes uses uint16 to store the primes, so it can only
generate primes up to 32768. That's (probably) enough for 8kB pages, but
for 32kB pages it's clearly insufficient.

Okay.

As for the original question how expensive this naive sieve is, I
haven't been able to measure any significant timings. The logging aroung
generate_primes usually looks like this:

2020-11-07 20:36:10.614 CET [232789] LOG:  generating primes nbits
575104 nhashes 3 target_partlen 191701
2020-11-07 20:36:10.614 CET [232789] LOG:  primes generated

So it takes 0.000 second for this extreme page size. I don't think we
need to invent anything more elaborate.

Okay, good to know. If we were concerned about memory, we could have it
check only odd numbers. That's a common feature of sieves, but also
makes the code a bit harder to understand if you haven't seen it before.

IMO if we were concerned about memory we'd use Bitmapset instead of an
array of bools. That's 1:8 compression, not just 1:2.

Also to fill in something I left for later, the reference for this

/* upper bound of number of primes below limit */
/* WIP: reference for this number */
int numprimes = 1.26 * limit / log(limit);

is

Rosser, J. Barkley; Schoenfeld, Lowell (1962). "Approximate formulas for
some functions of prime numbers". Illinois J. Math. 6: 64–94.
doi:10.1215/ijm/1255631807

More precisely, it's 30*log(113)/113 rounded up.

Thanks, I was wondering where that came from.

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#121John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#120)
Re: WIP: BRIN multi-range indexes

On Mon, Nov 9, 2020 at 1:39 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

While investigating the failures, I've tried increasing the values a
lot, without observing any measurable increase in runtime. IIRC I've
even used (10 * target_partlen) or something like that. That tells me
it's not very sensitive part of the code, so I'd suggest to simply use
something that we know is large enough to be safe.

Okay, then it's not worth being clever.

For example, the largest bloom filter we can have is 32kB, i.e. 262kb,
at which point the largest gap is less than 95 (per the gap table). And
we may use up to BLOOM_MAX_NUM_PARTITIONS, so let's just use
BLOOM_MAX_NUM_PARTITIONS * 100

Sure.

FWIW I wonder if we should do something about bloom filters that we know
can get larger than page size. In the example I used, we know that
nbits=575104 is larger than page, so as the filter gets more full (and
thus more random and less compressible) it won't possibly fit. Maybe we
should reject that right away, instead of "delaying it" until later, on
the basis that it's easier to fix at CREATE INDEX time (compared to when
inserts/updates start failing at a random time).

Yeah, I'd be inclined to reject that right away.

The problem with this is of course that if the index is multi-column,
this may not be strict enough (i.e. each filter would fit independently,
but the whole index row is too large). But it's probably better to do at
least something, and maybe improve that later with some whole-row check.

A whole-row check would be nice, but I don't know how hard that would be.

As a Devil's advocate proposal, how awful would it be to not allow
multicolumn brin-bloom indexes?

--
John Naylor
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#122Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#121)
10 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

Attached is an updated version of the patch series, rebased on current
master, and results for benchmark comparing the various bloom variants.

The improvements are fairly minor:

1) Rejecting bloom filters that are clearly too large (larger than page)
early. This is imperfect, as it works for individual index keys, not the
whole row. But per discussion it seems useful.

2) I've added sort_mode opclass parameter, allowing disabling the sorted
mode the bloom indexes start in by default. I'm not convinced we should
commit this, I've needed this for the benchmarking.

The benchmarking compares the three parts with different Bloom variants:

0004 - single hash with mod by (nbits) and (nbits-1)
0005 - two independent hashes (two random seeds)
0006 - partitioned approach, proposed by John Naylor

I'm attaching the shell script used to run the benchmark, and a summary
of the results. The 0004 is used as a baseline, and the comparisons show
speedups for 0005 and 0006 relative to that (if you scroll to the
right). Essentially, green means "faster than 0004" while red means slower.

I don't think any of those approaches comes as a clearly superior. The
results for most queries are within 2%, which is mostly just noise.
There are cases where the differences are more significant (~10%), but
it's in either direction and if you compare duration of the whole
benchmark (by summing per-query averages) it's within 1% again.

For the "mismatch" case (i.e. looking for values not contained in the
table) the differences are larger, but that's mostly due to luck and
hitting false positives for that particular query - on average the
differences are negligible, just like for the "match" case.

So based on this I'm tempted to just use the version with two hashes, as
implemented in 0005. It's much simpler than the partitioning scheme,
does not need any of the logic to generate primes etc.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0008-Ignore-correlation-for-new-BRIN-opclasses-20201220.patchtext/x-patch; charset=UTF-8; name=0008-Ignore-correlation-for-new-BRIN-opclasses-20201220.patchDownload
From 123091f50c8632eff994849cd61f181541ceed09 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 8/8] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 87a3d5598a..4e0712cfb2 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -817,6 +817,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index f9a7ae6608..4f6262f241 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1306,6 +1306,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 80bd60f876..e76593043f 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index ee4d0706df..67aea62a02 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0007-BRIN-minmax-multi-indexes-20201220.patchtext/x-patch; charset=UTF-8; name=0007-BRIN-minmax-multi-indexes-20201220.patchDownload
From 51f2925355afad4d9e031d79e52b068278aa7c14 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:32:59 +0100
Subject: [PATCH 7/8] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2389 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4792 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 8db10b7b1e..fb9e140b7a 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -590,6 +590,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..f9a7ae6608
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2389 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 17e50de530..e115d3b577 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -495,6 +503,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +587,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index b02298c0aa..03499281c6 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e3c9d47503..ee4d0706df 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index a9ccc3995b..064a93d09b 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 2f1f144db4..d3d12a7b99 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 048d682145..e72fcd125f 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1907,6 +1907,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -1984,6 +2130,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2010,6 +2173,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2077,6 +2256,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2108,6 +2353,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2130,6 +2392,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2152,6 +2431,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2216,6 +2512,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2367,6 +2680,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2422,6 +2881,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2444,6 +2920,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2500,6 +2993,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2522,6 +3032,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2588,6 +3115,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 3a59454ff1..6262d9e8d4 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -957,6 +957,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1052,6 +1198,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1098,6 +1261,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1148,6 +1328,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1201,6 +1455,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1235,6 +1509,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1268,6 +1562,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1353,6 +1667,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1478,6 +1811,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1548,6 +2045,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1582,6 +2099,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1642,6 +2179,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1673,7 +2230,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1728,6 +2306,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 922573e350..2b176640b7 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -277,18 +277,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -300,6 +309,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -308,33 +320,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -350,36 +380,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -391,6 +439,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -400,6 +451,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -409,6 +463,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index d92a32e5b9..9f5f2844d4 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 512f150b0e..456afd9802 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8141,6 +8141,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11053,3 +11118,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 3c037d1bfc..c417ebfba5 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -635,3 +635,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 5284f4198a..5ce9793337 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 69c39a1ca6..5050980cd4 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 6388bc7843..aa8bd4437e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0006-use-one-hash-bloom-variant-20201220.patchtext/x-patch; charset=UTF-8; name=0006-use-one-hash-bloom-variant-20201220.patchDownload
From c17877025f8296b6040eee1593da88eee25014bf Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Mon, 2 Nov 2020 23:55:28 +0100
Subject: [PATCH 6/8] use one-hash bloom variant

one-hash tweaks

tweak generation of primes
---
 src/backend/access/brin/brin_bloom.c | 179 ++++++++++++++++++++++++---
 1 file changed, 159 insertions(+), 20 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index f7b405f76f..87a3d5598a 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -208,6 +208,9 @@ typedef struct BloomOptions
 #define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
 #define		BLOOM_DEFAULT_SORT_MODE			true		/* start in sort */
 
+/* With the minimum allowed false positive rate of 0.001, we need up to 10 hashes */
+#define		BLOOM_MAX_NUM_PARTITIONS	10
+
 #define BloomGetNDistinctPerRange(opts) \
 	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
 	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
@@ -279,6 +282,7 @@ typedef struct BloomFilter
 	uint8	nhashes;	/* number of hash functions */
 	uint32	nbits;		/* number of bits in the bitmap (size) */
 	uint32	nbits_set;	/* number of bits set to 1 */
+	uint32	partlens[BLOOM_MAX_NUM_PARTITIONS];	/* partition lengths */
 
 	/* data of the bloom filter (used both for sorted and hashed phase) */
 	char	data[FLEXIBLE_ARRAY_MEMBER];
@@ -288,6 +292,127 @@ typedef struct BloomFilter
 static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
 
 
+/*
+ * generate_primes
+ * 		returns array of all primes less than limit
+ *
+ * WIP: very naive prime sieve; could be optimized using segmented ranges
+ */
+static uint32 *
+generate_primes(int limit)
+{
+	/* upper bound of number of primes below limit */
+	/* WIP: reference for this number */
+	int numprimes = 1.26 * limit / log(limit);
+
+	bool *is_composite = (bool *) palloc0(limit * sizeof(bool));
+	uint32 *primes = (uint32 *) palloc0(numprimes * sizeof(uint32));
+
+	int maxfactor = floor(sqrt(limit));
+	int factor = 2;	/* first prime */
+
+	/* mark the sieve where the index is composite */
+	while (factor < maxfactor)
+	{
+		for (int i = factor * factor; i < limit; i += factor)
+			 is_composite[i] = true;
+		do { factor++; } while (is_composite[factor]);
+	}
+
+	/* the unmarked numbers are prime, so copy over */
+	for (int i = 2, j = 0; i < limit && j < numprimes; i++)
+	{
+		if (!is_composite[i])
+			primes[j++] = i;
+	}
+
+	/* there should still be some zeroes at the end, but make sure */
+	primes[numprimes - 1] = 0;
+
+	/* pretty large, so free it now (segmented ranges would make it smaller) */
+	pfree(is_composite);
+	return primes;
+}
+
+/*
+ * set_bloom_partitions
+ * 		Calculate k moduli for one-hashing bloom filter.
+ *
+ * Find consecutive primes whose sum is close to nbits and
+ * return the sum. Copy the primes to the filter to use as
+ * partition lengths.
+ * WIP: one-hashing bf paper ref somewhere
+ */
+static uint32
+set_bloom_partitions(int nbits, uint8 nhashes, uint32 *partlens)
+{
+	int		min, diff, incr;
+	int		pidx = 0;
+	int		sum = 0;
+
+	/* we want partitions roughly with this length */
+	int		target_partlen = nbits / nhashes;
+	uint32 *primes;
+
+	/*
+	 * Generate primes up to a maximum value, based on the target length.
+	 * How much higher it needs to be is based on gaps between primes,
+	 * as shown at: https://primes.utm.edu/notes/gaps.html
+	 *
+	 * The largest possible filter is 32kB (the largest page size), i.e.
+	 * ~262kb. Assuming a single partition of this length, the maximum
+	 * gap is 95. With more partitions the gaps would be smaller, so we
+	 * just use this as an upper boundary, and multiply it with the max
+	 * number of partitions.
+	 *
+	 * Note: This is an overkill, but the impact on CPU time is minimal,
+	 * particularly with "regular" filter sizes. If needed, we can make
+	 * this more efficient in the future.
+	 */
+	primes = generate_primes(target_partlen + BLOOM_MAX_NUM_PARTITIONS * 100);
+
+	/*
+	 * In our array of primes, find a sequence of length nhashes, whose
+	 * last item is close to our target partition length. The end of the
+	 * array will be filled with zeros, so we need to guard against that.
+	 */
+	while (primes[pidx + nhashes - 1] <= target_partlen &&
+		   primes[pidx + nhashes] > 0)
+		pidx++;
+
+	for (int i = 0; i < nhashes; i++)
+		sum += primes[pidx + i];
+
+	/*
+	 * Since all the primes are less than or equal the desired partition
+	 * length, the sum is somewhat less than nbits. Increment the starting
+	 * point until we find the sequence of primes whose sum is closest to
+	 * nbits. It doesn't matter whether it's higher or lower.
+	 */
+	min = abs(nbits - sum);
+	for (;;)
+	{
+		incr = primes[pidx + nhashes] - primes[pidx];
+		diff = abs(nbits - (sum + incr));
+		if (diff >= min)
+			break;
+
+		min = diff;
+		sum += incr;
+		pidx++;
+	}
+
+	memcpy(partlens, &primes[pidx], nhashes * sizeof(uint32));
+
+	/* WIP: assuming it's not important to pfree primes */
+
+	/*
+	 * The actual filter length will be the sum of the partition lengths
+	 * rounded up to the nearest byte.
+	 */
+	return (uint32) ((sum + 7) / 8) * 8;
+}
+
 /*
  * bloom_init
  * 		Initialize the Bloom Filter, allocate all the memory.
@@ -319,6 +444,7 @@ bloom_init(bool sort_mode, int ndistinct, double false_positive_rate)
 	 */
 	k = log(2.0) * m / ndistinct;
 	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+	k = Min(k, BLOOM_MAX_NUM_PARTITIONS);
 
 	/*
 	 * When sort phase is enabled, start with a small filter which we grow
@@ -338,7 +464,9 @@ bloom_init(bool sort_mode, int ndistinct, double false_positive_rate)
 
 	filter->flags = 0;
 	filter->nhashes = (int) k;
-	filter->nbits = m;
+
+	/* calculate the partition lengths and adjust m to match */
+	filter->nbits = set_bloom_partitions(m, k, filter->partlens);
 
 	if (!sort_mode)
 		filter->flags |= BLOOM_FLAG_PHASE_HASH;
@@ -463,7 +591,7 @@ static BloomFilter *
 bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 {
 	int		i;
-	uint32	h1, h2;
+	int		part_boundary = 0;
 
 	/* assume 'not updated' by default */
 	Assert(filter);
@@ -532,17 +660,16 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 	/* we better be in the hashing phase */
 	Assert(BLOOM_IS_HASHED(filter));
 
-	/* compute the hashes, used for the bloom filter */
-	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
-	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
-
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		/* h1 + h2 + f(i) */
-		uint32	h = (h1 + i * h2) % filter->nbits;
-		uint32	byte = (h / 8);
-		uint32	bit  = (h % 8);
+		int partlen = filter->partlens[i];
+		int bitloc = part_boundary + (value % partlen);
+
+		int byte = (bitloc / 8);
+		int bit  = (bitloc % 8);
+
+		Assert(bitloc < filter->nbits);
 
 		/* if the bit is not set, set it and remember we did that */
 		if (! (filter->data[byte] & (0x01 << bit)))
@@ -552,6 +679,9 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 			if (updated)
 				*updated = true;
 		}
+
+		/* next bit */
+		part_boundary += partlen;
 	}
 
 	return filter;
@@ -581,6 +711,7 @@ bloom_switch_to_hashing(BloomFilter *filter)
 
 	newfilter->nhashes = filter->nhashes;
 	newfilter->nbits = filter->nbits;
+	memcpy(newfilter->partlens, filter->partlens, filter->nhashes * sizeof(uint32));
 	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
 
 	SET_VARSIZE(newfilter, len);
@@ -605,7 +736,7 @@ static bool
 bloom_contains_value(BloomFilter *filter, uint32 value)
 {
 	int		i;
-	uint32	h1, h2;
+	int		part_boundary = 0;
 
 	Assert(filter);
 
@@ -634,21 +765,23 @@ bloom_contains_value(BloomFilter *filter, uint32 value)
 	/* now the regular hashing mode */
 	Assert(BLOOM_IS_HASHED(filter));
 
-	/* calculate the two hashes */
-	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
-	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
-
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		/* h1 + h2 + f(i) */
-		uint32	h = (h1 + i * h2) % filter->nbits;
-		uint32	byte = (h / 8);
-		uint32	bit  = (h % 8);
+		int partlen = filter->partlens[i];
+		int bitloc = part_boundary + (value % partlen);
+
+		int byte = (bitloc / 8);
+		int bit  = (bitloc % 8);
+
+		Assert(bitloc < filter->nbits);
 
 		/* if the bit is not set, the value is not there */
 		if (! (filter->data[byte] & (0x01 << bit)))
 			return false;
+
+		/* next bit */
+		part_boundary += partlen;
 	}
 
 	/* all hashes found in bloom filter */
@@ -1081,8 +1214,14 @@ brin_bloom_summary_out(PG_FUNCTION_ARGS)
 
 	if (BLOOM_IS_HASHED(filter))
 	{
-		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+		appendStringInfo(&str,
+						 "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u  partition lengths:  [",
 						 filter->nhashes, filter->nbits, filter->nbits_set);
+		for (int i = 0; i < filter->nhashes - 1; i++)
+		{
+			appendStringInfo(&str, "%u, ", filter->partlens[i]);
+		}
+		appendStringInfo(&str, "%u]", filter->partlens[filter->nhashes - 1]);
 	}
 	else
 	{
-- 
2.26.2

0005-use-two-independent-hashes-20201220.patchtext/x-patch; charset=UTF-8; name=0005-use-two-independent-hashes-20201220.patchDownload
From 6461a80163d7744e1aa55315b09d9a329fd34014 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Thu, 17 Dec 2020 00:46:43 +0100
Subject: [PATCH 5/8] use two independent hashes

---
 src/backend/access/brin/brin_bloom.c | 45 ++++++++++------------------
 1 file changed, 15 insertions(+), 30 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 0cd6899a55..f7b405f76f 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -463,7 +463,7 @@ static BloomFilter *
 bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 {
 	int		i;
-	uint32	big_h, h, d;
+	uint32	h1, h2;
 
 	/* assume 'not updated' by default */
 	Assert(filter);
@@ -533,16 +533,16 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 	Assert(BLOOM_IS_HASHED(filter));
 
 	/* compute the hashes, used for the bloom filter */
-	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
-
-	h = big_h % filter->nbits;
-	d = big_h % (filter->nbits - 1);
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
 
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		int byte = (h / 8);
-		int bit  = (h % 8);
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
 
 		/* if the bit is not set, set it and remember we did that */
 		if (! (filter->data[byte] & (0x01 << bit)))
@@ -552,14 +552,6 @@ bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
 			if (updated)
 				*updated = true;
 		}
-
-		/* next bit */
-		h += d++;
-		if (h >= filter->nbits)
-			h -= filter->nbits;
-
-		if (d == filter->nbits)
-			d = 0;
 	}
 
 	return filter;
@@ -613,7 +605,7 @@ static bool
 bloom_contains_value(BloomFilter *filter, uint32 value)
 {
 	int		i;
-	uint32	big_h, h, d;
+	uint32	h1, h2;
 
 	Assert(filter);
 
@@ -642,28 +634,21 @@ bloom_contains_value(BloomFilter *filter, uint32 value)
 	/* now the regular hashing mode */
 	Assert(BLOOM_IS_HASHED(filter));
 
-	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
-
-	h = big_h % filter->nbits;
-	d = big_h % (filter->nbits - 1);
+	/* calculate the two hashes */
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
 
 	/* compute the requested number of hashes */
 	for (i = 0; i < filter->nhashes; i++)
 	{
-		int byte = (h / 8);
-		int bit  = (h % 8);
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
 
 		/* if the bit is not set, the value is not there */
 		if (! (filter->data[byte] & (0x01 << bit)))
 			return false;
-
-		/* next bit */
-		h += d++;
-		if (h >= filter->nbits)
-			h -= filter->nbits;
-
-		if (d == filter->nbits)
-			d = 0;
 	}
 
 	/* all hashes found in bloom filter */
-- 
2.26.2

0004-BRIN-bloom-indexes-20201220.patchtext/x-patch; charset=UTF-8; name=0004-BRIN-bloom-indexes-20201220.patchDownload
From e07d0ced23bf84846853bf9a7dccf7aad4c5b9f8 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 4/8] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com

add sort_mode opclass parameter

reject bloom filters larger than page
---
 doc/src/sgml/brin.sgml                    |  178 ++++
 doc/src/sgml/ref/create_index.sgml        |   31 +
 src/backend/access/brin/Makefile          |    1 +
 src/backend/access/brin/brin_bloom.c      | 1139 +++++++++++++++++++++
 src/include/access/brin.h                 |    2 +
 src/include/access/brin_internal.h        |    4 +
 src/include/catalog/pg_amop.dat           |  170 +++
 src/include/catalog/pg_amproc.dat         |  447 ++++++++
 src/include/catalog/pg_opclass.dat        |   72 ++
 src/include/catalog/pg_opfamily.dat       |   38 +
 src/include/catalog/pg_proc.dat           |   34 +
 src/include/catalog/pg_type.dat           |    7 +
 src/test/regress/expected/brin_bloom.out  |  456 +++++++++
 src/test/regress/expected/opr_sanity.out  |    3 +-
 src/test/regress/expected/psql.out        |    3 +-
 src/test/regress/expected/type_sanity.out |    7 +-
 src/test/regress/parallel_schedule        |    5 +
 src/test/regress/serial_schedule          |    1 +
 src/test/regress/sql/brin_bloom.sql       |  404 ++++++++
 19 files changed, 2997 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 2054d5d943..8db10b7b1e 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -559,6 +559,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..0cd6899a55
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,1139 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+	bool		sortMode;			/* start in sort mode */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.001		/* 0.1% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.1			/* 10% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+#define		BLOOM_DEFAULT_SORT_MODE			true		/* start in sort */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+#define BloomGetSortMode(opts) \
+	((opts) ? (((BloomOptions *) (opts))->sortMode) : \
+	 BLOOM_DEFAULT_SORT_MODE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(bool sort_mode, int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * When sort phase is enabled, start with a small filter which we grow
+	 * as needed. We use about 64B, which gives us ~40B in the bitmap part.
+	 * Otherwise allocate the whole filter in hash mode.
+	 */
+	if (sort_mode)
+		len = Max(offsetof(BloomFilter, data), 64);
+	else
+		len = offsetof(BloomFilter, data) + (m / 8);
+
+	/* Reject filters that are obviously too large to store on a page. */
+	if (len > BLCKSZ)
+		elog(ERROR, "the bloom filter is too large (%zu > %d)", len, BLCKSZ);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	if (!sort_mode)
+		filter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i, j, k;
+	Size	len;
+
+	uint32 *values;
+	uint32 *result;
+	uint32 *sorted;
+	uint32 *unsorted;
+
+	int		nvalues;
+	int		nsorted;
+	int		nunsorted;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* when already fully sorted, no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* result of merging */
+	k = 0;
+	result = (uint32 *) palloc(sizeof(uint32) * filter->nvalues);
+
+	/* first we have sorted data, then unsorted */
+	sorted = values;
+	nsorted = filter->nsorted;
+
+	unsorted = &values[filter->nsorted];
+	nunsorted = filter->nvalues - filter->nsorted;
+
+	/* sort the unsorted part, then merge the two parts */
+	pg_qsort(unsorted, nunsorted, sizeof(uint32), cmp_uint32);
+
+	i = 0;	/* sorted index */
+	j = 0;	/* unsorted index */
+
+	while ((i < nsorted) && (j < nunsorted))
+	{
+		if (sorted[i] <= unsorted[j])
+			result[k++] = sorted[i++];
+		else
+			result[k++] = unsorted[j++];
+	}
+
+	while (i < nsorted)
+		result[k++] = sorted[i++];
+
+	while (j < nunsorted)
+		result[k++] = unsorted[j++];
+
+	/* compact - remove duplicate values */
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (result[i] != result[nvalues - 1])
+			result[nvalues++] = result[i];
+	}
+
+	memcpy(filter->data, result, sizeof(uint32) * nvalues);
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	big_h = ((uint32) DatumGetInt64(hash_uint32(value)));
+
+	h = big_h % filter->nbits;
+	d = big_h % (filter->nbits - 1);
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+		h += d++;
+		if (h >= filter->nbits)
+			h -= filter->nbits;
+
+		if (d == filter->nbits)
+			d = 0;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+static bool
+brin_bloom_get_sort_mode(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetSortMode(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_sort_mode(bdesc, opts),
+							brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	add_local_bool_reloption(relopts, "sort_mode",
+							 "start the bloom filter in sort mode",
+							 BLOOM_DEFAULT_SORT_MODE,
+							 offsetof(BloomOptions, sortMode));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0987878dd2..b02298c0aa 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7c4f3da0a0..e3c9d47503 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 2c899f19d9..048d682145 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1712,6 +1712,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1729,6 +1734,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1746,6 +1756,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1892,6 +1907,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -1909,6 +1962,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -1926,6 +1984,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -1943,6 +2006,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2009,6 +2077,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2026,6 +2108,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2043,6 +2130,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2060,6 +2152,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2097,6 +2194,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2114,6 +2216,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2260,6 +2367,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2277,6 +2422,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2294,6 +2444,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2345,6 +2500,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2362,6 +2522,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2423,6 +2588,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index db3e8c2d01..3a59454ff1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -776,6 +776,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -789,6 +807,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -802,6 +838,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -903,6 +957,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -916,6 +1022,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -928,6 +1052,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -940,6 +1081,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -990,6 +1148,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1004,6 +1201,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1018,6 +1235,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1031,6 +1268,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1065,6 +1320,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1078,6 +1353,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1185,6 +1478,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1199,6 +1548,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1213,6 +1582,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1253,6 +1642,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1266,6 +1675,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1301,6 +1728,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index be5712692f..922573e350 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -259,67 +259,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -328,18 +391,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 11c7ad2c14..d92a32e5b9 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9c11c3af3a..512f150b0e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8158,6 +8158,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11019,3 +11039,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 28240bdce3..3c037d1bfc 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -628,3 +628,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..19b866283a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0009)
+);
+ERROR:  value 0.0009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "0.100000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.11)
+);
+ERROR:  value 0.11 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "0.100000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 507b474b1b..d966007c55 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2020,6 +2020,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2083,7 +2084,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 13567ddf84..5284f4198a 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ae89ed7f0b..69c39a1ca6 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -75,6 +75,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 525bdc804f..6388bc7843 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..3c2ef56316
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.11)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0003-Optimize-allocations-in-bringetbitmap-20201220.patchtext/x-patch; charset=UTF-8; name=0003-Optimize-allocations-in-bringetbitmap-20201220.patchDownload
From a0416df1404a927fcf81499c4a54b4e045c4fc00 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/8] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 4dd29c7b10..470431010d 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -372,6 +372,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -397,11 +400,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -437,9 +479,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -450,17 +492,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20201220.patchtext/x-patch; charset=UTF-8; name=0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20201220.patchDownload
From a97febd649354e5f62f61586d580b8b481d9b051 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/8] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 239 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index ccf609e799..4dd29c7b10 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -389,8 +367,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +395,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,23 +422,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -463,9 +447,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,15 +541,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -694,7 +734,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -723,25 +762,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1520,6 +1542,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1573,3 +1628,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index b1b3ce700d..cb4eb438d3 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -572,37 +504,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index b233558310..8886ad984e 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 9ffc9100c0..7c4f3da0a0 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20201220.patchtext/x-patch; charset=UTF-8; name=0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20201220.patchDownload
From 79177cd1c1a9b18418c8e3d918bd4285cb36ac03 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/8] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1f72562c60..ccf609e799 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +528,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,51 +538,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 986f76bd9b..b1b3ce700d 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +442,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +462,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +481,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +516,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +526,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 4b5d6a7213..b233558310 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index fb0615463e..0c28137c8f 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e6c7b070f6..9c11c3af3a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8135,7 +8135,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8151,7 +8151,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

results.odsapplication/vnd.oasis.opendocument.spreadsheet; name=results.odsDownload
brin-bloom-test.shapplication/x-shellscript; name=brin-bloom-test.shDownload
#123Thomas Munro
thomas.munro@gmail.com
In reply to: Tomas Vondra (#122)
Re: WIP: BRIN multi-range indexes

On Sun, Dec 20, 2020 at 1:16 PM Tomas Vondra
<tomas.vondra@enterprisedb.com> wrote:

Attached is an updated version of the patch series, rebased on current
master, and results for benchmark comparing the various bloom variants.

Perhaps src/include/utils/inet.h needs to include <sys/socket.h>,
because FreeBSD says:

brin_minmax_multi.c:1693:24: error: use of undeclared identifier 'AF_INET'
if (ip_family(ipa) == PGSQL_AF_INET)
^
../../../../src/include/utils/inet.h:39:24: note: expanded from macro
'PGSQL_AF_INET'
#define PGSQL_AF_INET (AF_INET + 0)
^

#124Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Thomas Munro (#123)
Re: WIP: BRIN multi-range indexes

Hi,

On 1/2/21 7:42 AM, Thomas Munro wrote:

On Sun, Dec 20, 2020 at 1:16 PM Tomas Vondra
<tomas.vondra@enterprisedb.com> wrote:

Attached is an updated version of the patch series, rebased on current
master, and results for benchmark comparing the various bloom variants.

Perhaps src/include/utils/inet.h needs to include <sys/socket.h>,
because FreeBSD says:

brin_minmax_multi.c:1693:24: error: use of undeclared identifier 'AF_INET'
if (ip_family(ipa) == PGSQL_AF_INET)
^
../../../../src/include/utils/inet.h:39:24: note: expanded from macro
'PGSQL_AF_INET'
#define PGSQL_AF_INET (AF_INET + 0)
^

Not sure. The other files using PGSQL_AF_INET just include sys/socket.h
directly, so maybe this should just do the same thing ...

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#125John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#122)
Re: WIP: BRIN multi-range indexes

On Sat, Dec 19, 2020 at 8:15 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

[12-20 version]

Hi Tomas,

The measurements look good. In case it fell through the cracks, my earlier
review comments for Bloom BRIN indexes regarding minor details don't seem
to have been addressed in this version. I'll point to earlier discussion
for convenience:

/messages/by-id/CACPNZCt=x-fOL0CUJbjR3BFXKgcd9HMPaRUVY9cwRe58hmd8Xg@mail.gmail.com

/messages/by-id/CACPNZCuqpkCGt8=cywAk1kPu0OoV_TjPXeV-J639ABQWyViyug@mail.gmail.com

The improvements are fairly minor:

1) Rejecting bloom filters that are clearly too large (larger than page)
early. This is imperfect, as it works for individual index keys, not the
whole row. But per discussion it seems useful.

I think this is good enough.

So based on this I'm tempted to just use the version with two hashes, as
implemented in 0005. It's much simpler than the partitioning scheme,
does not need any of the logic to generate primes etc.

Sounds like the best engineering decision.

Circling back to multi-minmax build times, I ran a couple quick tests on
bigger hardware, and found that not only is multi-minmax slower than
minmax, which is to be expected, but also slower than btree. (unlogged
table ~12GB in size, maintenance_work_mem = 1GB, median of three runs)

btree 38.3s
minmax 26.2s
multi-minmax 110s

Since btree indexes are much larger, I imagine something algorithmic is
involved. Is it worth digging further to see if some code path is taking
more time than we would expect?

--
John Naylor
EDB: http://www.enterprisedb.com

#126Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#125)
Re: WIP: BRIN multi-range indexes

On 1/12/21 6:28 PM, John Naylor wrote:

On Sat, Dec 19, 2020 at 8:15 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

[12-20 version]

Hi Tomas,

The measurements look good. In case it fell through the cracks, my
earlier review comments for Bloom BRIN indexes regarding minor details
don't seem to have been addressed in this version. I'll point to earlier
discussion for convenience:

/messages/by-id/CACPNZCt=x-fOL0CUJbjR3BFXKgcd9HMPaRUVY9cwRe58hmd8Xg@mail.gmail.com
</messages/by-id/CACPNZCt=x-fOL0CUJbjR3BFXKgcd9HMPaRUVY9cwRe58hmd8Xg@mail.gmail.com&gt;

/messages/by-id/CACPNZCuqpkCGt8=cywAk1kPu0OoV_TjPXeV-J639ABQWyViyug@mail.gmail.com
</messages/by-id/CACPNZCuqpkCGt8=cywAk1kPu0OoV_TjPXeV-J639ABQWyViyug@mail.gmail.com&gt;

Whooops :-( I'll go through those again, thanks for reminding me.

The improvements are fairly minor:

1) Rejecting bloom filters that are clearly too large (larger than page)
early. This is imperfect, as it works for individual index keys, not the
whole row. But per discussion it seems useful.

I think this is good enough.

So based on this I'm tempted to just use the version with two hashes, as
implemented in 0005. It's much simpler than the partitioning scheme,
does not need any of the logic to generate primes etc.

Sounds like the best engineering decision.

Circling back to multi-minmax build times, I ran a couple quick tests on
bigger hardware, and found that not only is multi-minmax slower than
minmax, which is to be expected, but also slower than btree. (unlogged
table ~12GB in size, maintenance_work_mem = 1GB, median of three runs)

btree          38.3s
minmax         26.2s
multi-minmax  110s

Since btree indexes are much larger, I imagine something algorithmic is
involved. Is it worth digging further to see if some code path is taking
more time than we would expect?

I suspect it'd due to minmax having to decide which "ranges" to merge,
which requires repeated sorting, etc. I certainly don't dare to claim
the current algorithm is perfect. I wouldn't have expected such big
difference, though - so definitely worth investigating.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#127Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#125)
8 attachment(s)
Re: WIP: BRIN multi-range indexes

On 1/12/21 6:28 PM, John Naylor wrote:

On Sat, Dec 19, 2020 at 8:15 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

[12-20 version]

Hi Tomas,

The measurements look good. In case it fell through the cracks, my
earlier review comments for Bloom BRIN indexes regarding minor details
don't seem to have been addressed in this version. I'll point to earlier
discussion for convenience:

/messages/by-id/CACPNZCt=x-fOL0CUJbjR3BFXKgcd9HMPaRUVY9cwRe58hmd8Xg@mail.gmail.com
</messages/by-id/CACPNZCt=x-fOL0CUJbjR3BFXKgcd9HMPaRUVY9cwRe58hmd8Xg@mail.gmail.com&gt;

/messages/by-id/CACPNZCuqpkCGt8=cywAk1kPu0OoV_TjPXeV-J639ABQWyViyug@mail.gmail.com
</messages/by-id/CACPNZCuqpkCGt8=cywAk1kPu0OoV_TjPXeV-J639ABQWyViyug@mail.gmail.com&gt;

Attached is a patch, addressing those issues - particularly those from
the first link, the second one is mostly a discussion about how to do
the hashing properly etc. It also switches to the two-hash variant, as
discussed earlier.

I've changed the range to allow false positives between 0.0001 and 0.25,
instead the original range (0.001 and 0.1). The default (0.01) remains
the same. I was worried that the original range was too narrow, and
would prevent even sensible combinations of parameter values. But now
that we reject bloom filters that are obviously too large, it's less of
an issue I think.

I'm not entirely convinced the sort_mode option should be committed. It
was meant only to allow benchmarking the hash approaches. In fact, I'm
thinking about removing the sorted mode entirely - if the bloom filter
contains only a few distinct values:

a) it's going to be almost entirely 0 bits, so easy to compress

b) it does not eliminate collisions entirely (we store hashes, not the
original values)

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210112.patchtext/x-patch; charset=UTF-8; name=0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210112.patchDownload
From d09d97018559747a04881f66ec8ca1dce0ef4ca8 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/8] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 58fe109d2d..8a1a9da78f 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +528,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,51 +538,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..215bc794d3 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +442,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +462,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +481,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +516,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +526,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..12878ff3a0 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..11835d85cd 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d7b55f57ea..871e024e00 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8143,7 +8143,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8159,7 +8159,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210112.patchtext/x-patch; charset=UTF-8; name=0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210112.patchDownload
From dffda10d21ef344d34580d87ba50f039eba1cbef Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/8] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 239 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 8a1a9da78f..55851376d8 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -389,8 +367,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +395,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,23 +422,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -463,9 +447,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,15 +541,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -694,7 +734,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -723,25 +762,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1520,6 +1542,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1573,3 +1628,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 215bc794d3..f4730be3b9 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -572,37 +504,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 12878ff3a0..6c8852d404 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 85c612e490..7b79a52536 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0003-Optimize-allocations-in-bringetbitmap-20210112.patchtext/x-patch; charset=UTF-8; name=0003-Optimize-allocations-in-bringetbitmap-20210112.patchDownload
From 3f12f8f7bd762e80a7a4b6695ed59c76f6f4eace Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/8] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 55851376d8..8d23f2864b 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -372,6 +372,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -397,11 +400,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -437,9 +479,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -450,17 +492,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0004-BRIN-bloom-indexes-20210112.patchtext/x-patch; charset=UTF-8; name=0004-BRIN-bloom-indexes-20210112.patchDownload
From 6e56ea2c70cd0c04e3b805338a724c4210c868a2 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 4/8] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    |  178 ++++
 doc/src/sgml/ref/create_index.sgml        |   31 +
 src/backend/access/brin/Makefile          |    1 +
 src/backend/access/brin/brin_bloom.c      | 1101 +++++++++++++++++++++
 src/include/access/brin.h                 |    2 +
 src/include/access/brin_internal.h        |    4 +
 src/include/catalog/pg_amop.dat           |  170 ++++
 src/include/catalog/pg_amproc.dat         |  447 +++++++++
 src/include/catalog/pg_opclass.dat        |   72 ++
 src/include/catalog/pg_opfamily.dat       |   38 +
 src/include/catalog/pg_proc.dat           |   34 +
 src/include/catalog/pg_type.dat           |    7 +
 src/test/regress/expected/brin_bloom.out  |  456 +++++++++
 src/test/regress/expected/opr_sanity.out  |    3 +-
 src/test/regress/expected/psql.out        |    3 +-
 src/test/regress/expected/type_sanity.out |    7 +-
 src/test/regress/parallel_schedule        |    5 +
 src/test/regress/serial_schedule          |    1 +
 src/test/regress/sql/brin_bloom.sql       |  404 ++++++++
 19 files changed, 2959 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 2054d5d943..8db10b7b1e 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -559,6 +559,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>128</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..b7aa6d9f11
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,1101 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient test whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it containst values
+ * we get later.
+ *
+ * The index only supports equality operator, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - either stored as is
+ * (in the sorted mode), or hashed again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements. For example we don't need to
+ * store varlena types differently in the sorted mode, etc. Everything is
+ * uint32 making it much simpler.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * The number of distinct elements (in a page range) depends on the data,
+ * we can consider it fixed. This simplifies the trade-off to just false
+ * positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't quite the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the table ndistinct estimate.
+ *
+ *
+ * sort vs. hash
+ * -------------
+ *
+ * As explained in the preceding section, sizing bloom filters correctly is
+ * difficult in practice. It's also true that bloom filters quickly degrade
+ * after exceeding the expected number of distinct items. It's therefore
+ * expected that people will overshoot the parameters a bit (particularly
+ * the ndistinct per page range), perhaps by a factor of 2 or more.
+ *
+ * It's also possible the data set is not uniform - it may have ranges with
+ * very many distinct items per range, but also ranges with only very few
+ * distinct items. The bloom filter has to be sized for the more variable
+ * ranges, making it rather wasteful in the less variable part.
+ *
+ * For example, if some page ranges have 1000 distinct values, that means
+ * about ~1.2kB bloom filter with 1% false positive rate. For page ranges
+ * that only contain 10 distinct values, that's wasteful, because we might
+ * store the values (the uint32 hashes) in just 40B.
+ *
+ * To address these issues, the opclass stores the raw values directly, and
+ * only switches to the actual bloom filter after reaching the same space
+ * requirements.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Flags. We only use a single bit for now, to decide whether we're in the
+ * sorted or hash phase. So we have 15 bits for future use, if needed. The
+ * filters are expected to be hundreds of bytes, so this is negligible.
+ */
+#define		BLOOM_FLAG_PHASE_HASH		0x0001
+
+#define		BLOOM_IS_HASHED(f)		((f)->flags & BLOOM_FLAG_PHASE_HASH)
+#define		BLOOM_IS_SORTED(f)		(!BLOOM_IS_HASHED(f))
+
+/*
+ * Number of hashes to accumulate before deduplicating and sorting in the
+ * sort phase? We want this fairly small to reduce the amount of space and
+ * also speed-up bsearch lookups.
+ */
+#define		BLOOM_MAX_UNSORTED		32
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.001		/* 0.1% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.1			/* 10% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (phase, etc.) */
+	uint16	flags;
+
+	/* fields used only in the SORTED phase */
+	uint16	nvalues;	/* number of hashes stored (total) */
+	uint16	nsorted;	/* number of hashes in the sorted part */
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter (used both for sorted and hashed phase) */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 *
+	 * XXX Maybe the 64B min size is not really needed?
+	 */
+	len = Max(offsetof(BloomFilter, data), 64);
+
+	/* Reject filters that are obviously too large to store on a page. */
+	if (len > BLCKSZ)
+		elog(ERROR, "the bloom filter is too large (%zu > %d)", len, BLCKSZ);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;	/* implies SORTED phase */
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * We do however set the varlena length, to minimize the storage needs.
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i, j, k;
+	Size	len;
+
+	uint32 *values;
+	uint32 *result;
+	uint32 *sorted;
+	uint32 *unsorted;
+
+	int		nvalues;
+	int		nsorted;
+	int		nunsorted;
+
+	/* never call compact on filters in HASH phase */
+	Assert(BLOOM_IS_SORTED(filter));
+
+	/* when already fully sorted, no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) filter->data;
+
+	/* result of merging */
+	k = 0;
+	result = (uint32 *) palloc(sizeof(uint32) * filter->nvalues);
+
+	/* first we have sorted data, then unsorted */
+	sorted = values;
+	nsorted = filter->nsorted;
+
+	unsorted = &values[filter->nsorted];
+	nunsorted = filter->nvalues - filter->nsorted;
+
+	/* sort the unsorted part, then merge the two parts */
+	pg_qsort(unsorted, nunsorted, sizeof(uint32), cmp_uint32);
+
+	i = 0;	/* sorted index */
+	j = 0;	/* unsorted index */
+
+	while ((i < nsorted) && (j < nunsorted))
+	{
+		if (sorted[i] <= unsorted[j])
+			result[k++] = sorted[i++];
+		else
+			result[k++] = unsorted[j++];
+	}
+
+	while (i < nsorted)
+		result[k++] = sorted[i++];
+
+	while (j < nunsorted)
+		result[k++] = unsorted[j++];
+
+	/* compact - remove duplicate values */
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if different from the last value, keep it */
+		if (result[i] != result[nvalues - 1])
+			result[nvalues++] = result[i];
+	}
+
+	memcpy(filter->data, result, sizeof(uint32) * nvalues);
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	len = offsetof(BloomFilter, data) +
+				   (filter->nvalues) * sizeof(uint32);
+
+	SET_VARSIZE(filter, len);
+}
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	h1, h2;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter, data) +
+						(filter->nvalues + 1) * sizeof(uint32);
+
+			/*
+			 * We don't double the size here, as in the first place we care about
+			 * reducing storage requirements, and the doubling happens automatically
+			 * in memory contexts (so the repalloc should be cheap in most cases).
+			 */
+			if (len < need)
+			{
+				filter = (BloomFilter *) repalloc(filter, need);
+				SET_VARSIZE(filter, need);
+			}
+
+			/* copy the new value into the filter */
+			memcpy(&filter->data[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	Assert(filter->nbits % 8 == 0);
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, data) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->flags |= BLOOM_FLAG_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->data;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	h1, h2;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (BLOOM_IS_SORTED(filter))
+	{
+		int i;
+		uint32 *values = (uint32 *) filter->data;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+		{
+			if (value == values[i])
+				return true;
+		}
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(BLOOM_IS_HASHED(filter));
+
+	/* calculate the two hashes */
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+static double
+brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
+{
+	return BloomGetFalsePositiveRate(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							brin_bloom_get_fp_rate(bdesc, opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (BLOOM_IS_SORTED(filter_b))
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->data;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (BLOOM_IS_SORTED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->data;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (BLOOM_IS_HASHED(filter_a))
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		/* we better have "compatible" bloom filters */
+		Assert(filter_a->nbits == filter_b->nbits);
+		Assert(filter_a->nhashes == filter_b->nhashes);
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->data[i] |= filter_b->data[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	if (BLOOM_IS_HASHED(filter))
+	{
+		appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+						 filter->nhashes, filter->nbits, filter->nbits_set);
+	}
+	else
+	{
+		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
+						 filter->nvalues, filter->nsorted);
+		/* TODO include the sorted/unsorted values */
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7b79a52536..d114ac58cd 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 9339971e77..c48d07f8fc 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2064,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2086,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2108,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2179,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2210,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2232,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2254,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2296,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2318,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2469,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2524,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2546,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2602,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2624,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2690,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 9b90f4d806..d9308c1337 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -801,6 +801,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -814,6 +832,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -827,6 +863,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -928,6 +982,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -941,6 +1047,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -953,6 +1077,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -965,6 +1106,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1015,6 +1173,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1029,6 +1226,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1043,6 +1260,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1056,6 +1293,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1090,6 +1345,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1103,6 +1378,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1210,6 +1503,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1224,6 +1573,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1238,6 +1607,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1278,6 +1667,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1291,6 +1700,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1327,6 +1754,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 5128d6eded..b9e96bf27e 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -265,67 +265,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -334,18 +397,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..5c294bac89 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 871e024e00..c8c4f449d4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8166,6 +8166,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11315,3 +11335,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 49dc3e18e0..3275719896 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -673,3 +673,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..19b866283a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0009)
+);
+ERROR:  value 0.0009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "0.100000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.11)
+);
+ERROR:  value 0.11 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.001000" and "0.100000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..a44bde2e6e 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index e0e1ef71dd..ef372281ef 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 081fce32e7..fdddc48f62 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..3c2ef56316
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.001 and 1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.0009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.11)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0005-bloom-fixes-and-tweaks-20210112.patchtext/x-patch; charset=UTF-8; name=0005-bloom-fixes-and-tweaks-20210112.patchDownload
From ae62adf343f415cd473ae33c513bed5ceb6026dd Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 13 Jan 2021 00:59:02 +0100
Subject: [PATCH 5/8] bloom fixes and tweaks

---
 doc/src/sgml/ref/create_index.sgml       |  2 +-
 src/backend/access/brin/brin_bloom.c     | 84 ++++++++++++++----------
 src/test/regress/expected/brin_bloom.out | 14 ++--
 src/test/regress/sql/brin_bloom.sql      |  6 +-
 4 files changed, 60 insertions(+), 46 deletions(-)

diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 8db10b7b1e..244e5834d8 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -573,7 +573,7 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
      equal to -1, the number of distinct non-null is assumed linear with
      the maximum possible number of tuples in the block range (about 290
      rows per block). The default values is <literal>-0.1</literal>, and
-     the minimum number of distinct non-null values is <literal>128</literal>.
+     the minimum number of distinct non-null values is <literal>16</literal>.
     </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index b7aa6d9f11..ffeb459d3e 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -8,12 +8,12 @@
  *
  * A BRIN opclass summarizing page range into a bloom filter.
  *
- * Bloom filters allow efficient test whether a given page range contains
+ * Bloom filters allow efficient testing whether a given page range contains
  * a particular value. Therefore, if we summarize each page range into a
- * bloom filter, we can easily and cheaply test wheter it containst values
+ * bloom filter, we can easily and cheaply test wheter it contains values
  * we get later.
  *
- * The index only supports equality operator, similarly to hash indexes.
+ * The index only supports equality operators, similarly to hash indexes.
  * BRIN bloom indexes are however much smaller, and support only bitmap
  * scans.
  *
@@ -51,9 +51,9 @@
  * the bloom filter. On the other hand, we want to keep the index as small
  * as possible - that's one of the basic advantages of BRIN indexes.
  *
- * The number of distinct elements (in a page range) depends on the data,
- * we can consider it fixed. This simplifies the trade-off to just false
- * positive rate vs. size.
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
  *
  * At the page range level, false positive rate is a probability the bloom
  * filter matches a random value. For the whole index (with sufficiently
@@ -65,7 +65,7 @@
  * the bitmap is inherently random, compression can't reliably help here.
  * To reduce the size of a filter (to fit to a page), we have to either
  * accept higher false positive rate (undesirable), or reduce the number
- * of distinct items to be stored in the filter. We can't quite the input
+ * of distinct items to be stored in the filter. We can't alter the input
  * data, of course, but we may make the BRIN page ranges smaller - instead
  * of the default 128 pages (1MB) we may build index with 16-page ranges,
  * or something like that. This does help even for random data sets, as
@@ -87,7 +87,8 @@
  * not entirely clear how to distrubute the space between those columns.
  *
  * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
- * make some basic sizing decisions, based on the table ndistinct estimate.
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
  *
  *
  * sort vs. hash
@@ -200,10 +201,20 @@ typedef struct BloomOptions
 
 /*
  * Allowed range and default value for the false positive range. The exact
- * values are somewhat arbitrary.
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
  */
-#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.001		/* 0.1% fp rate */
-#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.1			/* 10% fp rate */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001		/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25		/* 25% fp rate */
 #define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
 
 #define BloomGetNDistinctPerRange(opts) \
@@ -302,11 +313,21 @@ bloom_init(int ndistinct, double false_positive_rate)
 	Assert(ndistinct > 0);
 	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
 
-	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	m = ceil(- (ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
 
 	/* round m to whole bytes */
 	m = ((m + 7) / 8) * 8;
 
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * We do expect the bloom filter to eventually switch to hashing mode,
+	 * and it's bound to be almost perfectly random, so not compressible.
+	 */
+	if ((m/8) > BLCKSZ)
+		elog(ERROR, "the bloom filter is too large (%d > %d)", (m/8), BLCKSZ);
+
 	/*
 	 * round(log(2.0) * m / ndistinct), but assume round() may not be
 	 * available on Windows
@@ -315,16 +336,10 @@ bloom_init(int ndistinct, double false_positive_rate)
 	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
 
 	/*
-	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
-	 * bitmap part). We require space at least for the header.
-	 *
-	 * XXX Maybe the 64B min size is not really needed?
+	 * Allocate the bloom filter (initially it's just a header, we'll make
+	 * it larger as needed).
 	 */
-	len = Max(offsetof(BloomFilter, data), 64);
-
-	/* Reject filters that are obviously too large to store on a page. */
-	if (len > BLCKSZ)
-		elog(ERROR, "the bloom filter is too large (%zu > %d)", len, BLCKSZ);
+	len = offsetof(BloomFilter, data);
 
 	filter = (BloomFilter *) palloc0(len);
 
@@ -686,7 +701,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
  * brin_bloom_get_ndistinct
  *		Determine the ndistinct value used to size bloom filter.
  *
- * Tweak the ndistinct value based on the pagesPerRange value. First,
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
  * if it's negative, it's assumed to be relative to maximum number of
  * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
  * tuples, which is likely a significant over-estimate). We also clamp
@@ -701,6 +716,10 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
  * and compute the expected number of distinct values in a range. But
  * that may be tricky due to data being sorted in various ways, so it
  * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
  */
 static int
 brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
@@ -738,11 +757,6 @@ brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
 	return (int) ndistinct;
 }
 
-static double
-brin_bloom_get_fp_rate(BrinDesc *bdesc, BloomOptions *opts)
-{
-	return BloomGetFalsePositiveRate(opts);
-}
 
 /*
  * Examine the given index tuple (which contains partial status of a certain
@@ -777,7 +791,7 @@ brin_bloom_add_value(PG_FUNCTION_ARGS)
 	if (column->bv_allnulls)
 	{
 		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
-							brin_bloom_get_fp_rate(bdesc, opts));
+							BloomGetFalsePositiveRate(opts));
 		column->bv_values[0] = PointerGetDatum(filter);
 		column->bv_allnulls = false;
 		updated = true;
@@ -949,7 +963,7 @@ brin_bloom_union(PG_FUNCTION_ARGS)
  * Cache and return inclusion opclass support procedure
  *
  * Return the procedure corresponding to the given function support number
- * or null if it is not exists.
+ * or null if it does not exists.
  */
 static FmgrInfo *
 bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
@@ -1050,11 +1064,8 @@ brin_bloom_summary_out(PG_FUNCTION_ARGS)
 	initStringInfo(&str);
 	appendStringInfoChar(&str, '{');
 
-	/*
-	 * XXX not sure the detoasting is necessary (probably not, this
-	 * can only be in an index).
-	 */
-	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+	/* Detoasting not needed (this can only be in an index). */
+	filter = (BloomFilter *) PG_GETARG_BYTEA_PP(0);
 
 	if (BLOOM_IS_HASHED(filter))
 	{
@@ -1063,9 +1074,12 @@ brin_bloom_summary_out(PG_FUNCTION_ARGS)
 	}
 	else
 	{
+		/*
+		 * XXX Maybe include the sorted/unsorted values? Seems a bit too
+		 * much useless detail (internal hash values).
+		 */
 		appendStringInfo(&str, "mode: sorted  nvalues: %u  nsorted: %u",
 						 filter->nvalues, filter->nsorted);
-		/* TODO include the sorted/unsorted values */
 	}
 
 	appendStringInfoChar(&str, '}');
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
index 19b866283a..24ea5f6e42 100644
--- a/src/test/regress/expected/brin_bloom.out
+++ b/src/test/regress/expected/brin_bloom.out
@@ -58,17 +58,17 @@ CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
 );
 ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
 DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
--- false_positive_rate must be between 0.001 and 1.0
+-- false_positive_rate must be between 0.0001 and 0.25
 CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
-	byteacol bytea_bloom_ops(false_positive_rate = 0.0009)
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
 );
-ERROR:  value 0.0009 out of bounds for option "false_positive_rate"
-DETAIL:  Valid values are between "0.001000" and "0.100000".
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
 CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
-	byteacol bytea_bloom_ops(false_positive_rate = 0.11)
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
 );
-ERROR:  value 0.11 out of bounds for option "false_positive_rate"
-DETAIL:  Valid values are between "0.001000" and "0.100000".
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
 CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
 	byteacol bytea_bloom_ops,
 	charcol char_bloom_ops,
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
index 3c2ef56316..d587f3962f 100644
--- a/src/test/regress/sql/brin_bloom.sql
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -59,12 +59,12 @@ FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
 CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
 	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
 );
--- false_positive_rate must be between 0.001 and 1.0
+-- false_positive_rate must be between 0.0001 and 0.25
 CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
-	byteacol bytea_bloom_ops(false_positive_rate = 0.0009)
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
 );
 CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
-	byteacol bytea_bloom_ops(false_positive_rate = 0.11)
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
 );
 
 CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
-- 
2.26.2

0006-add-sort_mode-opclass-parameter-20210112.patchtext/x-patch; charset=UTF-8; name=0006-add-sort_mode-opclass-parameter-20210112.patchDownload
From 55ce54180bcafd70ab984fb2d08cc8272099ea17 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 23:57:11 +0100
Subject: [PATCH 6/8] add sort_mode opclass parameter

---
 src/backend/access/brin/brin_bloom.c | 34 ++++++++++++++++++++++------
 1 file changed, 27 insertions(+), 7 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index ffeb459d3e..ac5a5c249c 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -180,6 +180,7 @@ typedef struct BloomOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	bool		sortMode;			/* start in sort mode */
 } BloomOptions;
 
 /*
@@ -216,6 +217,7 @@ typedef struct BloomOptions
 #define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001		/* 0.01% fp rate */
 #define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25		/* 25% fp rate */
 #define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+#define		BLOOM_DEFAULT_SORT_MODE			true		/* start in sort */
 
 #define BloomGetNDistinctPerRange(opts) \
 	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
@@ -227,6 +229,10 @@ typedef struct BloomOptions
 	 (((BloomOptions *) (opts))->falsePositiveRate) : \
 	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
 
+#define BloomGetSortMode(opts) \
+	((opts) ? (((BloomOptions *) (opts))->sortMode) : \
+	 BLOOM_DEFAULT_SORT_MODE)
+
 /*
  * Bloom Filter
  *
@@ -302,7 +308,7 @@ static BloomFilter *bloom_switch_to_hashing(BloomFilter *filter);
  * varlena.
  */
 static BloomFilter *
-bloom_init(int ndistinct, double false_positive_rate)
+bloom_init(bool sort_mode, int ndistinct, double false_positive_rate)
 {
 	Size			len;
 	BloomFilter	   *filter;
@@ -336,17 +342,26 @@ bloom_init(int ndistinct, double false_positive_rate)
 	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
 
 	/*
-	 * Allocate the bloom filter (initially it's just a header, we'll make
-	 * it larger as needed).
+	 * When sort phase is enabled, allocate just the header - we'll make
+	 * it larger as needed. In hash mode we allocate the whole filter.
+	 *
+	 * XXX We might add some sort of sparse bitmap, which might work for
+	 * bloom filters with only a couple items.
 	 */
-	len = offsetof(BloomFilter, data);
+	if (sort_mode)
+		len = offsetof(BloomFilter, data);
+	else
+		len = offsetof(BloomFilter, data) + (m / 8);
 
 	filter = (BloomFilter *) palloc0(len);
 
-	filter->flags = 0;	/* implies SORTED phase */
+	filter->flags = 0;
 	filter->nhashes = (int) k;
 	filter->nbits = m;
 
+	if (!sort_mode)
+		filter->flags |= BLOOM_FLAG_PHASE_HASH;
+
 	SET_VARSIZE(filter, len);
 
 	return filter;
@@ -757,7 +772,6 @@ brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
 	return (int) ndistinct;
 }
 
-
 /*
  * Examine the given index tuple (which contains partial status of a certain
  * page range) by comparing it to the given value that comes from another heap
@@ -790,7 +804,8 @@ brin_bloom_add_value(PG_FUNCTION_ARGS)
 	 */
 	if (column->bv_allnulls)
 	{
-		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+		filter = bloom_init(BloomGetSortMode(opts),
+							brin_bloom_get_ndistinct(bdesc, opts),
 							BloomGetFalsePositiveRate(opts));
 		column->bv_values[0] = PointerGetDatum(filter);
 		column->bv_allnulls = false;
@@ -1022,6 +1037,11 @@ brin_bloom_options(PG_FUNCTION_ARGS)
 							 BLOOM_MAX_FALSE_POSITIVE_RATE,
 							 offsetof(BloomOptions, falsePositiveRate));
 
+	add_local_bool_reloption(relopts, "sort_mode",
+							 "start the bloom filter in sort mode",
+							 BLOOM_DEFAULT_SORT_MODE,
+							 offsetof(BloomOptions, sortMode));
+
 	PG_RETURN_VOID();
 }
 
-- 
2.26.2

0007-BRIN-minmax-multi-indexes-20210112.patchtext/x-patch; charset=UTF-8; name=0007-BRIN-minmax-multi-indexes-20210112.patchDownload
From c63e71b0fc4d3dc3430ec0ef6a8541eb6c7faa97 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:32:59 +0100
Subject: [PATCH 7/8] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2389 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4792 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 244e5834d8..ce61a28a72 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -590,6 +590,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..f9a7ae6608
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2389 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * XXX not sure the detoasting is necessary (probably not, this
+	 * can only be in an index).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..df820db029 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -495,6 +503,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +587,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index d114ac58cd..e7ce4d0209 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..66a6c3c792 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index e07dd83550..e2a04d9cb0 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c48d07f8fc..791c755492 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2086,6 +2232,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2112,6 +2275,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2179,6 +2358,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2210,6 +2455,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2232,6 +2494,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2254,6 +2533,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2318,6 +2614,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2469,6 +2782,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2524,6 +2983,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2546,6 +3022,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2602,6 +3095,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2624,6 +3134,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2690,6 +3217,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index d9308c1337..41bd834770 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -982,6 +982,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1077,6 +1223,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1123,6 +1286,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1173,6 +1353,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1226,6 +1480,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1260,6 +1534,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1293,6 +1587,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1378,6 +1692,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1503,6 +1836,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1573,6 +2070,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1607,6 +2124,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1667,6 +2204,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1698,7 +2255,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1754,6 +2332,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index b9e96bf27e..e0e9651cb1 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -283,18 +283,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -306,6 +315,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -314,33 +326,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -356,36 +386,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -397,6 +445,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -406,6 +457,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -415,6 +469,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 5c294bac89..c00d6d6500 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c8c4f449d4..41f76e6104 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8149,6 +8149,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11349,3 +11414,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 3275719896..93b745757a 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -680,3 +680,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a44bde2e6e..0ca2671910 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ef372281ef..db078e5201 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index fdddc48f62..daf2e898cb 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -110,6 +110,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0008-Ignore-correlation-for-new-BRIN-opclasses-20210112.patchtext/x-patch; charset=UTF-8; name=0008-Ignore-correlation-for-new-BRIN-opclasses-20210112.patchDownload
From 92383ff313d228e3525b5b0a03e65603d115d43d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 8/8] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index ac5a5c249c..84d91ec0ce 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -703,6 +703,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index f9a7ae6608..4f6262f241 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1306,6 +1306,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d5e61664bc..d0cc145938 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e7ce4d0209..901febe375 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

#128Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#127)
6 attachment(s)
Re: WIP: BRIN multi-range indexes

Here is a slightly improved version of the patch series.

Firstly, I realized the PG_DETOAST_DATUM() in brin_bloom_summary_out is
actually needed - the value can't be toasted, but it might be stored
with just 1B header. So we need to expand it to 4B, because the struct
has int32 as the first field.

I've also removed the sort mode from bloom filters. I've thought about
this for a long time, and ultimately concluded that it's not worth the
extra complexity. It might work for ranges with very few distinct
values, but that also means the bloom filter will be mostly 0 and thus
easy to compress (and with very low false-positive rate). There probably
are cases where it might be a bit better/smaller, but I had a hard time
constructing such cases. So I ditched it for now. I've kept the "flags"
which is unused and reserved for future, to allow such improvements.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210114.patchtext/x-patch; charset=UTF-8; name=0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210114.patchDownload
From 56fc2270e1a6c787a31a1c44d780987fd400aa36 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/6] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 58fe109d2d..8a1a9da78f 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -389,6 +389,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -410,6 +413,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -470,7 +528,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -480,51 +538,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..215bc794d3 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +442,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +462,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +481,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +516,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +526,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..12878ff3a0 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..11835d85cd 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d7b55f57ea..871e024e00 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8143,7 +8143,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8159,7 +8159,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210114.patchtext/x-patch; charset=UTF-8; name=0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210114.patchDownload
From 4bdb681fb0173e5a12a716d3fad1fcd1ac42dbb6 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/6] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 239 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 8a1a9da78f..55851376d8 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -178,7 +181,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -242,31 +244,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -389,8 +367,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -415,10 +395,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -439,23 +422,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -463,9 +447,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -543,15 +541,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -694,7 +734,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -723,25 +762,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1520,6 +1542,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1573,3 +1628,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 215bc794d3..f4730be3b9 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -572,37 +504,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 12878ff3a0..6c8852d404 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 85c612e490..7b79a52536 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0003-Optimize-allocations-in-bringetbitmap-20210114.patchtext/x-patch; charset=UTF-8; name=0003-Optimize-allocations-in-bringetbitmap-20210114.patchDownload
From 631992f5d7a1a82578b287712d69d95644738c8a Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/6] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 55851376d8..8d23f2864b 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -372,6 +372,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -397,11 +400,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -437,9 +479,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -450,17 +492,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0004-BRIN-bloom-indexes-20210114.patchtext/x-patch; charset=UTF-8; name=0004-BRIN-bloom-indexes-20210114.patchDownload
From 9e856ce1d04eb426f5ea5b129146e0623c344de0 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 4/6] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 178 +++++
 doc/src/sgml/ref/create_index.sgml        |  31 +
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 759 ++++++++++++++++++++++
 src/include/access/brin.h                 |   2 +
 src/include/access/brin_internal.h        |   4 +
 src/include/catalog/pg_amop.dat           | 170 +++++
 src/include/catalog/pg_amproc.dat         | 447 +++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 ++
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +
 src/test/regress/expected/brin_bloom.out  | 456 +++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 404 ++++++++++++
 19 files changed, 2617 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 2054d5d943..244e5834d8 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -559,6 +559,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..000c2387ee
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,759 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001		/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25		/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * To calculate the additional hashes (treating the uint32 hash as an input
+ * value), we use the approach with two hash functions from this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions are calculated using hard-coded seeds.
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes
+ * that are not entirely 0. But while indexes don't support TOAST, the
+ * varlena can still be compressed. So this seems unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high. But even if the false positive rate
+ * exceeds the desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16	flags;
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	m = ceil(- (ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * We do expect the bloom filter to eventually switch to hashing mode,
+	 * and it's bound to be almost perfectly random, so not compressible.
+	 */
+	if ((m/8) > BLCKSZ)
+		elog(ERROR, "the bloom filter is too large (%d > %d)", (m/8), BLCKSZ);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so
+	 * the varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + (m / 8);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	h1, h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	h1, h2;
+
+	/* calculate the two hashes */
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int		i;
+	int		nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 7b79a52536..d114ac58cd 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 9339971e77..c48d07f8fc 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2064,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2086,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2108,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2179,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2210,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2232,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2254,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2296,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2318,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2469,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2524,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2546,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2602,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2624,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2690,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 9b90f4d806..d9308c1337 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -801,6 +801,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -814,6 +832,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -827,6 +863,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -928,6 +982,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -941,6 +1047,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -953,6 +1077,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -965,6 +1106,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1015,6 +1173,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1029,6 +1226,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1043,6 +1260,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1056,6 +1293,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1090,6 +1345,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1103,6 +1378,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1210,6 +1503,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1224,6 +1573,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1238,6 +1607,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1278,6 +1667,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1291,6 +1700,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1327,6 +1754,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 5128d6eded..b9e96bf27e 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -265,67 +265,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -334,18 +397,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..5c294bac89 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 871e024e00..c8c4f449d4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8166,6 +8166,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11315,3 +11335,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 49dc3e18e0..3275719896 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -673,3 +673,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..24ea5f6e42
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..a44bde2e6e 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index e0e1ef71dd..ef372281ef 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 081fce32e7..fdddc48f62 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..d587f3962f
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0005-BRIN-minmax-multi-indexes-20210114.patchtext/x-patch; charset=UTF-8; name=0005-BRIN-minmax-multi-indexes-20210114.patchDownload
From 58763b553536657e51214a9b5c9661215ece3621 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:32:59 +0100
Subject: [PATCH 5/6] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2389 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4792 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 244e5834d8..ce61a28a72 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -590,6 +590,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..0a76557cc1
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2389 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..df820db029 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -495,6 +503,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +587,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index d114ac58cd..e7ce4d0209 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..66a6c3c792 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index e07dd83550..e2a04d9cb0 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c48d07f8fc..791c755492 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2086,6 +2232,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2112,6 +2275,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2179,6 +2358,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2210,6 +2455,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2232,6 +2494,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2254,6 +2533,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2318,6 +2614,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2469,6 +2782,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2524,6 +2983,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2546,6 +3022,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2602,6 +3095,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2624,6 +3134,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2690,6 +3217,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index d9308c1337..41bd834770 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -982,6 +982,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1077,6 +1223,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1123,6 +1286,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1173,6 +1353,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1226,6 +1480,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1260,6 +1534,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1293,6 +1587,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1378,6 +1692,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1503,6 +1836,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1573,6 +2070,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1607,6 +2124,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1667,6 +2204,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1698,7 +2255,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1754,6 +2332,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index b9e96bf27e..e0e9651cb1 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -283,18 +283,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -306,6 +315,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -314,33 +326,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -356,36 +386,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -397,6 +445,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -406,6 +457,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -415,6 +469,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 5c294bac89..c00d6d6500 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c8c4f449d4..41f76e6104 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8149,6 +8149,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11349,3 +11414,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 3275719896..93b745757a 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -680,3 +680,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a44bde2e6e..0ca2671910 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ef372281ef..db078e5201 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index fdddc48f62..daf2e898cb 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -110,6 +110,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0006-Ignore-correlation-for-new-BRIN-opclasses-20210114.patchtext/x-patch; charset=UTF-8; name=0006-Ignore-correlation-for-new-BRIN-opclasses-20210114.patchDownload
From c41e6ed957ca8ae3609628a55ff596428ae1836c Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 6/6] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 000c2387ee..0137095660 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -385,6 +385,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 0a76557cc1..d1f29934fc 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1306,6 +1306,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d5e61664bc..d0cc145938 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e7ce4d0209..901febe375 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

#129Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#128)
6 attachment(s)
Re: WIP: BRIN multi-range indexes

A version (hopefully) fixing the issue with build on FreeBSD, identified
by commitfest.cputube.org.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-Pass-all-scan-keys-to-BRIN-consistent-fun-20210114-2.patchtext/x-patch; charset=UTF-8; name=0001-Pass-all-scan-keys-to-BRIN-consistent-fun-20210114-2.patchDownload
From 5cb1da056d8833c4c5aa23b6583d8e174b5b5f70 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/6] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 27ba596c6e..dc187153aa 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -390,6 +390,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -411,6 +414,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -471,7 +529,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -481,51 +539,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..215bc794d3 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +442,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +462,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +481,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +516,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +526,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..12878ff3a0 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..11835d85cd 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d27336adcd..f3ead7d53d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8145,7 +8145,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8161,7 +8161,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-suppo-20210114-2.patchtext/x-patch; charset=UTF-8; name=0002-Move-IS-NOT-NULL-handling-from-BRIN-suppo-20210114-2.patchDownload
From c1ca49afd9ae59f6aa1043f352de23e81d4ee995 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/6] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 239 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index dc187153aa..14da9ed17f 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -179,7 +182,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -243,31 +245,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -390,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -416,10 +396,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -440,23 +423,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -464,9 +448,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -544,15 +542,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -695,7 +735,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -724,25 +763,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1521,6 +1543,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1574,3 +1629,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 215bc794d3..f4730be3b9 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -572,37 +504,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 12878ff3a0..6c8852d404 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 78c89a6961..79440ebe7b 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0003-Optimize-allocations-in-bringetbitmap-20210114-2.patchtext/x-patch; charset=UTF-8; name=0003-Optimize-allocations-in-bringetbitmap-20210114-2.patchDownload
From ab83c8b194fb6eba20de831f5033e58cf26ed22c Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/6] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 14da9ed17f..3735c41788 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -398,11 +401,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -438,9 +480,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -451,17 +493,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0004-BRIN-bloom-indexes-20210114-2.patchtext/x-patch; charset=UTF-8; name=0004-BRIN-bloom-indexes-20210114-2.patchDownload
From f9ab56883a44710c2aac3570f9508429a44be0e2 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 4/6] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 178 +++++
 doc/src/sgml/ref/create_index.sgml        |  31 +
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 759 ++++++++++++++++++++++
 src/include/access/brin.h                 |   2 +
 src/include/access/brin_internal.h        |   4 +
 src/include/catalog/pg_amop.dat           | 170 +++++
 src/include/catalog/pg_amproc.dat         | 447 +++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 ++
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +
 src/test/regress/expected/brin_bloom.out  | 456 +++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 404 ++++++++++++
 19 files changed, 2617 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index a5271a9f8f..5eed3d0ab2 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -581,6 +581,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..000c2387ee
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,759 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001		/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25		/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * To calculate the additional hashes (treating the uint32 hash as an input
+ * value), we use the approach with two hash functions from this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions are calculated using hard-coded seeds.
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes
+ * that are not entirely 0. But while indexes don't support TOAST, the
+ * varlena can still be compressed. So this seems unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high. But even if the false positive rate
+ * exceeds the desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16	flags;
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	m = ceil(- (ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * We do expect the bloom filter to eventually switch to hashing mode,
+	 * and it's bound to be almost perfectly random, so not compressible.
+	 */
+	if ((m/8) > BLCKSZ)
+		elog(ERROR, "the bloom filter is too large (%d > %d)", (m/8), BLCKSZ);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so
+	 * the varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + (m / 8);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	h1, h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	h1, h2;
+
+	/* calculate the two hashes */
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int		i;
+	int		nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 79440ebe7b..8cc4e532e6 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..616329df8f 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2064,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2086,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2108,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2179,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2210,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2232,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2254,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2296,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2318,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2469,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2524,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2546,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2602,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2624,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2690,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..6709c8dfea 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..5c294bac89 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f3ead7d53d..cab6ae2f23 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8168,6 +8168,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11338,3 +11358,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 56da2913bd..8c857d5cc1 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -680,3 +680,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..24ea5f6e42
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..a44bde2e6e 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index e0e1ef71dd..ef372281ef 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 081fce32e7..fdddc48f62 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..d587f3962f
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0005-BRIN-minmax-multi-indexes-20210114-2.patchtext/x-patch; charset=UTF-8; name=0005-BRIN-minmax-multi-indexes-20210114-2.patchDownload
From a46cf0b7212f96af50e793e6f9c8eb8942f342e4 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:32:59 +0100
Subject: [PATCH 5/6] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2392 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4795 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 5eed3d0ab2..03bbf23390 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -612,6 +612,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..bd17da8cd5
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2392 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..df820db029 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -495,6 +503,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +587,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 8cc4e532e6..fdaff42722 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..66a6c3c792 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index e07dd83550..e2a04d9cb0 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 616329df8f..762ac3acef 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2086,6 +2232,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2112,6 +2275,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2179,6 +2358,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2210,6 +2455,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2232,6 +2494,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2254,6 +2533,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2318,6 +2614,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2469,6 +2782,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2524,6 +2983,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2546,6 +3022,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2602,6 +3095,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2624,6 +3134,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2690,6 +3217,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 6709c8dfea..0111777a23 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 5c294bac89..c00d6d6500 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cab6ae2f23..a92b04f335 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8151,6 +8151,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11372,3 +11437,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8c857d5cc1..a94dc9be75 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -687,3 +687,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a44bde2e6e..0ca2671910 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ef372281ef..db078e5201 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index fdddc48f62..daf2e898cb 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -110,6 +110,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0006-Ignore-correlation-for-new-BRIN-opclasses-20210114-2.patchtext/x-patch; charset=UTF-8; name=0006-Ignore-correlation-for-new-BRIN-opclasses-20210114-2.patchDownload
From f26ef24bfe6b397ff27a88ce1d57571bd38d2b94 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 6/6] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 000c2387ee..0137095660 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -385,6 +385,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index bd17da8cd5..6ef5c5f2bf 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1309,6 +1309,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d5e61664bc..d0cc145938 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index fdaff42722..5fbf8cf9c7 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

#130John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#126)
Re: WIP: BRIN multi-range indexes

On Tue, Jan 12, 2021 at 1:42 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

I suspect it'd due to minmax having to decide which "ranges" to merge,
which requires repeated sorting, etc. I certainly don't dare to claim
the current algorithm is perfect. I wouldn't have expected such big
difference, though - so definitely worth investigating.

It seems that monotonically increasing (or decreasing) values in a table
are a worst case scenario for multi-minmax indexes, or basically, unique
values within a range. I'm guessing it's because it requires many passes
to fit all the values into a limited number of ranges. I tried using
smaller pages_per_range numbers, 32 and 8, and that didn't help.

Now, with a different data distribution, using only 10 values that repeat
over and over, the results are much more sympathetic to multi-minmax:

insert into iot (num, create_dt)
select random(), '2020-01-01 0:00'::timestamptz + (x % 10 || '
seconds')::interval
from generate_series(1,5*365*24*60*60) x;

create index cd_single on iot using brin(create_dt);
27.2s

create index cd_multi on iot using brin(create_dt
timestamptz_minmax_multi_ops);
30.4s

create index cd_bt on iot using btree(create_dt);
61.8s

Circling back to the monotonic case, I tried running a simple perf record
on a backend creating a multi-minmax index on a timestamptz column and
these were the highest non-kernel calls:
+   21.98%    21.91%  postgres         postgres            [.]
FunctionCall2Coll
+    9.31%     9.29%  postgres         postgres            [.]
compare_combine_ranges
+    8.60%     8.58%  postgres         postgres            [.] qsort_arg
+    5.68%     5.66%  postgres         postgres            [.]
brin_minmax_multi_add_value
+    5.63%     5.60%  postgres         postgres            [.] timestamp_lt
+    4.73%     4.71%  postgres         postgres            [.]
reduce_combine_ranges
+    3.80%     0.00%  postgres         [unknown]           [.]
0x0320016800040000
+    3.51%     3.50%  postgres         postgres            [.] timestamp_eq

There's no one place that's pathological enough to explain the 4x slowness
over traditional BRIN and nearly 3x slowness over btree when using a large
number of unique values per range, so making progress here would have to
involve a more holistic approach.

--
John Naylor
EDB: http://www.enterprisedb.com

#131Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#130)
Re: WIP: BRIN multi-range indexes

On 1/19/21 9:44 PM, John Naylor wrote:

On Tue, Jan 12, 2021 at 1:42 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

I suspect it'd due to minmax having to decide which "ranges" to merge,
which requires repeated sorting, etc. I certainly don't dare to claim
the current algorithm is perfect. I wouldn't have expected such big
difference, though - so definitely worth investigating.

It seems that monotonically increasing (or decreasing) values in a table
are a worst case scenario for multi-minmax indexes, or basically, unique
values within a range. I'm guessing it's because it requires many passes
to fit all the values into a limited number of ranges. I tried using
smaller pages_per_range numbers, 32 and 8, and that didn't help.

Now, with a different data distribution, using only 10 values that
repeat over and over, the results are muchs more sympathetic to multi-minmax:

insert into iot (num, create_dt)
select random(), '2020-01-01 0:00'::timestamptz + (x % 10 || '
seconds')::interval
from generate_series(1,5*365*24*60*60) x;

create index cd_single on iot using brin(create_dt);
27.2s

create index cd_multi on iot using brin(create_dt
timestamptz_minmax_multi_ops);
30.4s

create index cd_bt on iot using btree(create_dt);
61.8s

Circling back to the monotonic case, I tried running a simple perf 
record on a backend creating a multi-minmax index on a timestamptz 
column and these were the highest non-kernel calls:
+   21.98%    21.91%  postgres         postgres            [.] 
FunctionCall2Coll
+    9.31%     9.29%  postgres         postgres            [.] 
compare_combine_ranges
+    8.60%     8.58%  postgres         postgres            [.] qsort_arg
+    5.68%     5.66%  postgres         postgres            [.] 
brin_minmax_multi_add_value
+    5.63%     5.60%  postgres         postgres            [.] timestamp_lt
+    4.73%     4.71%  postgres         postgres            [.] 
reduce_combine_ranges
+    3.80%     0.00%  postgres         [unknown]           [.] 
0x0320016800040000
+    3.51%     3.50%  postgres         postgres            [.] timestamp_eq

There's no one place that's pathological enough to explain the 4x
slowness over traditional BRIN and nearly 3x slowness over btree when
using a large number of unique values per range, so making progress here
would have to involve a more holistic approach.

Yeah. This very much seems like the primary problem is in how we build
the ranges incrementally - with monotonic sequences, we end up having to
merge the ranges over and over again. I don't know what was the
structure of the table, but I guess it was kinda narrow (very few
columns), which exacerbates the problem further, because the number of
rows per range will be way higher than in real-world.

I do think the solution to this might be to allow more values during
batch index creation, and only "compress" to the requested number at the
very end (when serializing to on-disk format).

There are a couple additional comments about possibly replacing
sequential scan with a binary search, that could help a bit too.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#132Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#131)
9 attachment(s)
Re: WIP: BRIN multi-range indexes

On 1/20/21 1:07 AM, Tomas Vondra wrote:

On 1/19/21 9:44 PM, John Naylor wrote:

On Tue, Jan 12, 2021 at 1:42 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:
 > I suspect it'd due to minmax having to decide which "ranges" to merge,
 > which requires repeated sorting, etc. I certainly don't dare to claim
 > the current algorithm is perfect. I wouldn't have expected such big
 > difference, though - so definitely worth investigating.

It seems that monotonically increasing (or decreasing) values in a
table are a worst case scenario for multi-minmax indexes, or
basically, unique values within a range. I'm guessing it's because it
requires many passes to fit all the values into a limited number of
ranges. I tried using smaller pages_per_range numbers, 32 and 8, and
that didn't help.

Now, with a different data distribution, using only 10 values that
repeat over and over, the results are muchs more sympathetic to
multi-minmax:

insert into iot (num, create_dt)
select random(), '2020-01-01 0:00'::timestamptz + (x % 10 || '
seconds')::interval
from generate_series(1,5*365*24*60*60) x;

create index cd_single on iot using brin(create_dt);
27.2s

create index cd_multi on iot using brin(create_dt
timestamptz_minmax_multi_ops);
30.4s

create index cd_bt on iot using btree(create_dt);
61.8s

Circling back to the monotonic case, I tried running a simple perf 
record on a backend creating a multi-minmax index on a timestamptz 
column and these were the highest non-kernel calls:
+   21.98%    21.91%  postgres         postgres            [.] 
FunctionCall2Coll
+    9.31%     9.29%  postgres         postgres            [.] 
compare_combine_ranges
+    8.60%     8.58%  postgres         postgres            [.] qsort_arg
+    5.68%     5.66%  postgres         postgres            [.] 
brin_minmax_multi_add_value
+    5.63%     5.60%  postgres         postgres            [.] 
timestamp_lt
+    4.73%     4.71%  postgres         postgres            [.] 
reduce_combine_ranges
+    3.80%     0.00%  postgres         [unknown]           [.] 
0x0320016800040000
+    3.51%     3.50%  postgres         postgres            [.] 
timestamp_eq

There's no one place that's pathological enough to explain the 4x
slowness over traditional BRIN and nearly 3x slowness over btree when
using a large number of unique values per range, so making progress
here would have to involve a more holistic approach.

Yeah. This very much seems like the primary problem is in how we build
the ranges incrementally - with monotonic sequences, we end up having to
merge the ranges over and over again. I don't know what was the
structure of the table, but I guess it was kinda narrow (very few
columns), which exacerbates the problem further, because the number of
rows per range will be way higher than in real-world.

I do think the solution to this might be to allow more values during
batch index creation, and only "compress" to the requested number at the
very end (when serializing to on-disk format).

There are a couple additional comments about possibly replacing

sequential scan with a binary search, that could help a bit too.

OK, I took a look at this, and I came up with two optimizations that
improve this for the pathological cases. I've kept this as patches on
top of the last patch, to allow easier review of the changes.

0007 - This reworks how the ranges are reduced by merging the closest
ranges. Instead of doing that iteratively in a fairly expensive loop,
the new reduce reduce_combine_ranges() uses much simpler approach.

There's a couple more optimizations (skipping expensive code when not
needed, etc.) which should help a bit too.

0008 - This is a WIP version of the batch mode. Originally, when
building an index we'd "fill" the small buffer, combine some of the
ranges to free ~25% of space for new values. And we'd do this over and
over. This involves some expensive steps (sorting etc.) and for some
pathologic cases (like monotonic sequences) this performed particularly
poorly. The new code simply collects all values in the range, and then
does the expensive stuff only once.

Note: These parts are fairly new, with minimal testing so far.

When measured on a table with 10M rows with a number of data sets with
different patterns, the results look like this:

dataset btree minmax unpatched patched diff
--------------------------------------------------------------
monotonic-100-asc 3023 1002 1281 1722 1.34
monotonic-100-desc 3245 1042 1363 1674 1.23
monotonic-10000-asc 2597 1028 2469 2272 0.92
monotonic-10000-desc 2591 1021 2157 2246 1.04
monotonic-asc 1863 968 4884 1106 0.23
monotonic-desc 2546 1017 3520 2337 0.66
random-100 3648 1133 1594 1797 1.13
random-10000 3507 1124 1651 2657 1.61

The btree and minmax are the current indexes. unpatched means minmax
multi from the previous patch version, patched is with 0007 and 0008
applied. The diff shows patched/unpatched. The benchmarking script is
attached.

The pathological case (monotonic-asc) is now 4x faster, roughly equal to
regular minmax index build. The opposite (monotonic-desc) is about 33%
faster, roughly in line with btree.

There are a couple cases where it's actually a bit slower - those are
the cases with very few distinct values per range. I believe this
happens because in the batch mode the code does not check if the summary
already contains this value, adds it to the buffer and the last step
ends up being more expensive than this.

I believe there's some "compromise" between those two extremes, i.e. we
should use buffer that is too small or too large, but something in
between, so that the reduction happens once in a while but not too often
(as with the original aggressive approach).

FWIW, none of this is likely to be an issue in practice, because (a)
tables usually don't have such strictly monotonic patterns, (b) people
should stick to plain minmax for cases that do. And (c) regular tables
tend to have much wider rows, so there are fewer values per range (so
that other stuff is likely more expensive that building BRIN).

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210122.patchtext/x-patch; charset=UTF-8; name=0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210122.patchDownload
From 670cc60d75d6c99dbecf19f480fdfe10f2a9023f Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/8] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 27ba596c6e..dc187153aa 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -390,6 +390,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -411,6 +414,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -471,7 +529,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -481,51 +539,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..215bc794d3 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +442,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +462,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +481,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +516,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +526,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..12878ff3a0 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..11835d85cd 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b5f52d4e4a..2ef11245a8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8183,7 +8183,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8199,7 +8199,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210122.patchtext/x-patch; charset=UTF-8; name=0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210122.patchDownload
From 84e05ba149c262aa5c52278fa91b12f7af25c12e Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/8] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 239 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index dc187153aa..14da9ed17f 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -179,7 +182,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -243,31 +245,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -390,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -416,10 +396,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -440,23 +423,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -464,9 +448,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -544,15 +542,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -695,7 +735,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -724,25 +763,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1521,6 +1543,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1574,3 +1629,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 215bc794d3..f4730be3b9 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -572,37 +504,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 12878ff3a0..6c8852d404 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 78c89a6961..79440ebe7b 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0003-Optimize-allocations-in-bringetbitmap-20210122.patchtext/x-patch; charset=UTF-8; name=0003-Optimize-allocations-in-bringetbitmap-20210122.patchDownload
From e02e445c3541784c0cfe3725a419c7f3318a000f Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/8] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 14da9ed17f..3735c41788 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -398,11 +401,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -438,9 +480,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -451,17 +493,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0004-BRIN-bloom-indexes-20210122.patchtext/x-patch; charset=UTF-8; name=0004-BRIN-bloom-indexes-20210122.patchDownload
From 19ebc2da49a940e0c1f653fc5c914e5df1883fe1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 4/8] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 178 +++++
 doc/src/sgml/ref/create_index.sgml        |  31 +
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 759 ++++++++++++++++++++++
 src/include/access/brin.h                 |   2 +
 src/include/access/brin_internal.h        |   4 +
 src/include/catalog/pg_amop.dat           | 170 +++++
 src/include/catalog/pg_amproc.dat         | 447 +++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 ++
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +
 src/test/regress/expected/brin_bloom.out  | 456 +++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 404 ++++++++++++
 19 files changed, 2617 insertions(+), 5 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index a5271a9f8f..5eed3d0ab2 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -581,6 +581,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default values is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default values is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..000c2387ee
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,759 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test wheter it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001		/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25		/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * To calculate the additional hashes (treating the uint32 hash as an input
+ * value), we use the approach with two hash functions from this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions are calculated using hard-coded seeds.
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes
+ * that are not entirely 0. But while indexes don't support TOAST, the
+ * varlena can still be compressed. So this seems unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high. But even if the false positive rate
+ * exceeds the desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16	flags;
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		m;	/* number of bits */
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	m = ceil(- (ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * We do expect the bloom filter to eventually switch to hashing mode,
+	 * and it's bound to be almost perfectly random, so not compressible.
+	 */
+	if ((m/8) > BLCKSZ)
+		elog(ERROR, "the bloom filter is too large (%d > %d)", (m/8), BLCKSZ);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * m / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so
+	 * the varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + (m / 8);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	h1, h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	h1, h2;
+
+	/* calculate the two hashes */
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int		i;
+	int		nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 79440ebe7b..8cc4e532e6 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..616329df8f 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2064,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2086,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2108,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2179,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2210,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2232,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2254,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2296,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2318,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2469,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2524,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2546,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2602,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2624,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2690,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..6709c8dfea 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..5c294bac89 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '8100',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '8101',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '8102',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '8103',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '8104',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '8105',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '8106',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '8107',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '8108',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '8123',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '8109',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '8110',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '8111',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '8112',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '8113',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '8114',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '8115',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '8116',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '8117',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2ef11245a8..0b702546e0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8206,6 +8206,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '8118', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '8119', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '8120', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '8121', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '8122', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11376,3 +11396,17 @@
   prosrc => 'unicode_is_normalized' },
 
 ]
+
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 56da2913bd..8c857d5cc1 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -680,3 +680,10 @@
   typalign => 'd', typstorage => 'x' },
 
 ]
+
+{ oid => '9034',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..24ea5f6e42
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..a44bde2e6e 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9034 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index e0e1ef71dd..ef372281ef 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 081fce32e7..fdddc48f62 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..d587f3962f
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0005-BRIN-minmax-multi-indexes-20210122.patchtext/x-patch; charset=UTF-8; name=0005-BRIN-minmax-multi-indexes-20210122.patchDownload
From ebe481c2f2cccaff9ebfe0b087a1b3e684009676 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:32:59 +0100
Subject: [PATCH 5/8] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2392 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   17 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    4 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 +++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   80 +
 src/include/catalog/pg_type.dat             |    7 +
 src/test/regress/expected/brin_multi.out    |  421 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  371 +++
 21 files changed, 4795 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 5eed3d0ab2..03bbf23390 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -612,6 +612,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     16 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..bd17da8cd5
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2392 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (as set by values_per_range reloption).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+	int	count = count_values(cranges, ncranges);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count <= max_values * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
+			count -= 2;
+		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
+			count -= 1;
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the maxvalues threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX The maxvalues may be different, so perhaps use Max?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 16, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..df820db029 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -495,6 +503,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +587,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 8cc4e532e6..fdaff42722 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..66a6c3c792 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +28,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index e07dd83550..e2a04d9cb0 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 616329df8f..762ac3acef 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2086,6 +2232,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2112,6 +2275,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2179,6 +2358,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2210,6 +2455,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2232,6 +2494,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2254,6 +2533,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2318,6 +2614,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2469,6 +2782,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2524,6 +2983,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2546,6 +3022,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2602,6 +3095,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2624,6 +3134,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2690,6 +3217,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 6709c8dfea..0111777a23 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 5c294bac89..c00d6d6500 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9015',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '8100',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9016',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '8101',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9017',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '8103',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9018',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '8104',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9019',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '8108',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '8123',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9020',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9021',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '8109',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9022',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '8110',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9023',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '8111',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9024',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '8112',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9025',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '8114',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9026',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '8115',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9027',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '8116',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9028',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '8117',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0b702546e0..3754c90071 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8189,6 +8189,71 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9029', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9030', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9031', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9032', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9033', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9000', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9001', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9002', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9003', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9004', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9005', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9006', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9007', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9008', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9009', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9010', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9011', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9012', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9013', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9014', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11410,3 +11475,18 @@
 { oid => '9038', descr => 'I/O',
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
+
+{ oid => '9040', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9041', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9042', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9043', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8c857d5cc1..a94dc9be75 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -687,3 +687,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+
+{ oid => '9039',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..966dc4aadc
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,421 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+ERROR:  value 15 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "16" and "256".
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a44bde2e6e..0ca2671910 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9034 | pg_brin_bloom_summary
-(5 rows)
+ 9039 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ef372281ef..db078e5201 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index fdddc48f62..daf2e898cb 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -110,6 +110,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..9de6ddc6d7
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,371 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 15)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0006-Ignore-correlation-for-new-BRIN-opclasses-20210122.patchtext/x-patch; charset=UTF-8; name=0006-Ignore-correlation-for-new-BRIN-opclasses-20210122.patchDownload
From 35643130c00550db4d3d2793449cb0e25e68b4dc Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 6/8] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 000c2387ee..0137095660 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -385,6 +385,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index bd17da8cd5..6ef5c5f2bf 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1309,6 +1309,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d5e61664bc..d0cc145938 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index fdaff42722..5fbf8cf9c7 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0007-patched-2-20210122.patchtext/x-patch; charset=UTF-8; name=0007-patched-2-20210122.patchDownload
From 778764830c0143b219c2853907db78bb7a427ab3 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Fri, 22 Jan 2021 00:12:00 +0100
Subject: [PATCH 7/8] patched 2

---
 src/backend/access/brin/brin_minmax_multi.c | 284 +++++++++++++-------
 1 file changed, 189 insertions(+), 95 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 6ef5c5f2bf..094ecd2be9 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -90,6 +90,7 @@
  */
 #define		PROCNUM_BASE			11
 
+#define		MINMAX_LOAD_FACTOR		0.75
 
 typedef struct MinmaxMultiOpaque
 {
@@ -160,11 +161,11 @@ typedef struct Ranges
 } Ranges;
 
 /*
- * On-disk the summary is stored as a bytea value, represented by the
- * SerializedRanges structure. It has a 4B varlena header, so can treated
- * as varlena directly.
+ * On-disk the summary is stored as a bytea value, with a simple header
+ * with basic metadata, followed by the boundary values. It has a varlena
+ * header, so can be treated as varlena directly.
  *
- * See range_serialize/range_deserialize methods for serialization details.
+ * See range_serialize/range_deserialize for serialization details.
  */
 typedef struct SerializedRanges
 {
@@ -483,6 +484,32 @@ compare_combine_ranges(const void *a, const void *b, void *arg)
 	return 0;
 }
 
+/*
+ * compare_values
+ *	  Compare the values.
+ */
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum *da = (Datum *) a;
+	Datum *db = (Datum *) b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *da, *db);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *db, *da);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
 /*
  * range_contains_value
  * 		See if the new value is already contained in the range list.
@@ -923,6 +950,8 @@ typedef struct DistanceValue
 
 /*
  * Simple comparator for distance values, comparing the double value.
+ * This is intentionally sorting the distances in descending order, i.e.
+ * the longer gaps will be at the front.
  */
 static int
 compare_distances(const void *a, const void *b)
@@ -931,9 +960,9 @@ compare_distances(const void *a, const void *b)
 	DistanceValue *db = (DistanceValue *)b;
 
 	if (da->value < db->value)
-		return -1;
-	else if (da->value > db->value)
 		return 1;
+	else if (da->value > db->value)
+		return -1;
 
 	return 0;
 }
@@ -942,8 +971,8 @@ compare_distances(const void *a, const void *b)
  * Given an array of combine ranges, compute distance of the gaps betwen
  * the ranges - for ncranges there are (ncranges-1) gaps.
  *
- * We simply call the "distance" function to compute the (min-max) for pairs
- * of consecutive ganges. The function may be fairly expensive, so we do that
+ * We simply call the "distance" function to compute the (max-min) for pairs
+ * of consecutive ranges. The function may be fairly expensive, so we do that
  * just once (and then use it to pick as many ranges to merge as possible).
  *
  * See reduce_combine_ranges for details.
@@ -970,16 +999,20 @@ build_distances(FmgrInfo *distanceFn, Oid colloid,
 		a1 = cranges[i].maxval;
 		a2 = cranges[i+1].minval;
 
-		/* compute length of the empty gap (distance between max/min) */
+		/* compute length of the gap (between max/min) */
 		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
 
-		/* remember the index */
+		/* remember the index of the gap the distance is for */
 		distances[i].index = i;
 		distances[i].value = DatumGetFloat8(r);
 	}
 
-	/* sort the distances in ascending order */
-	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+	/*
+	 * Sort the distances in descending order, so that the longest gaps
+	 * are at the front.
+	 */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue),
+			 compare_distances);
 
 	return distances;
 }
@@ -1017,6 +1050,7 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
 	return cranges;
 }
 
+#ifdef USE_ASSERT_CHECKING
 /*
  * Counts bondary values needed to store the ranges. Each single-point
  * range is stored using a single value, each regular range needs two.
@@ -1038,10 +1072,26 @@ count_values(CombineRange *cranges, int ncranges)
 
 	return count;
 }
+#endif
 
 /*
- * Combines ranges until the number of boundary values drops below 75%
- * of the capacity (as set by values_per_range reloption).
+ * reduce_combine_ranges
+ *		reduce the ranges until the number of values is low enough
+ *
+ * Combines ranges until the number of boundary values drops below the
+ * threshold specified by max_values. This happens by merging enough
+ * ranges by distance between them.
+ *
+ * Returns the number of result ranges.
+ *
+ * We simply use the global min/max and then add boundaries for enough
+ * largest gaps. Each gap adds 2 values, so we simply use (target/2-1)
+ * distances. Then we simply sort all the values - each two values are
+ * a boundary of a range (possibly collapsed).
+ *
+ * XXX Some of the ranges may be collapsed (i.e. the min/max values are
+ * equal), but we ignore that for now. We could repeat the process,
+ * adding a couple more gaps recursively.
  *
  * XXX The ranges to merge are selected solely using the distance. But
  * that may not be the best strategy, for example when multiple gaps
@@ -1062,66 +1112,86 @@ count_values(CombineRange *cranges, int ncranges)
  * length of the ranges? Or perhaps randomize the choice of ranges, with
  * probability inversely proportional to the distance (the gap lengths
  * may be very close, but not exactly the same).
+ *
+ * XXX Or maybe we could just handle this by using random value as a
+ * tie-break, or by adding random noise to the actual distance.
  */
 static int
 reduce_combine_ranges(CombineRange *cranges, int ncranges,
-					  DistanceValue *distances, int max_values)
+					  DistanceValue *distances, int max_values,
+					  FmgrInfo *cmp, Oid colloid)
 {
 	int i;
+	int		nvalues;
+	Datum  *values;
+
+	compare_context cxt;
+
+	/* total number of gaps between ranges */
 	int	ndistances = (ncranges - 1);
-	int	count = count_values(cranges, ncranges);
+
+	/* number of gaps to keep */
+	int keep = (max_values/2 - 1);
 
 	/*
-	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
-	 * the number of combine ranges (reduction is the primary goal of
-	 * this function), so we must use a separate value.
+	 * Maybe we have sufficiently low number of ranges already?
+	 *
+	 * XXX This should happen before we actually do the expensive stuff
+	 * like sorting, so maybe this should be just an assert.
 	 */
-	for (i = 0; i < ndistances; i++)
-	{
-		int j;
-		int shortest;
+	if (keep >= ndistances)
+		return ncranges;
 
-		if (count <= max_values * 0.75)
-			break;
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
 
-		shortest = distances[i].index;
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
 
-		/*
-		 * The index must be still valid with respect to the current size
-		 * of cranges array (and it always points to the first range, so
-		 * never to the last one - hence the -1 in the condition).
-		 */
-		Assert(shortest < (ncranges - 1));
+	/* add the global min/max values, from the first/last range */
+	values[nvalues++] = cranges[0].minval;
+	values[nvalues++] = cranges[ncranges-1].maxval;
 
-		if (!cranges[shortest].collapsed && !cranges[shortest+1].collapsed)
-			count -= 2;
-		else if (!cranges[shortest].collapsed || !cranges[shortest+1].collapsed)
-			count -= 1;
+	/* add boundary values for enough gaps */
+	for (i = 0; i < keep; i++)
+	{
+		/* index of the gap between (index) and (index+1) ranges */
+		int index = distances[i].index;
 
-		/*
-		 * Move the values to join the two selected ranges. The new range is
-		 * definiely not collapsed but a regular range.
-		 */
-		cranges[shortest].maxval = cranges[shortest+1].maxval;
-		cranges[shortest].collapsed = false;
+		Assert((index >= 0) && ((index+1) < ncranges));
 
-		/* shuffle the subsequent combine ranges */
-		memmove(&cranges[shortest+1], &cranges[shortest+2],
-				(ncranges - shortest - 2) * sizeof(CombineRange));
+		/* add min from the preceding range, max from the next one */
+		values[nvalues++] = cranges[index].minval;
+		values[nvalues++] = cranges[index+1].minval;
 
-		/* also, shuffle all higher indexes (we've just moved the ranges) */
-		for (j = i; j < ndistances; j++)
-		{
-			if (distances[j].index > shortest)
-				distances[j].index--;
-		}
+		Assert(nvalues <= max_values);
+	}
 
-		ncranges--;
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/*
+	 * Sort the values using the comparator function, and form ranges
+	 * from the sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
 
-		Assert(ncranges > 0);
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		cranges[i].minval = values[2*i];
+		cranges[i].maxval = values[2*i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		cranges[i].collapsed = (compare_values(&values[2*i],
+											   &values[2*i+1],
+											   &cxt) == 0);
 	}
 
-	return ncranges;
+	return (nvalues / 2);
 }
 
 /*
@@ -1251,43 +1321,58 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
 					 ranges->nvalues);
 
-	/* and we'll also need the 'distance' procedure */
-	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
 
-	/*
-	 * The distanceFn calls (which may internally call e.g. numeric_le) may
-	 * allocate quite a bit of memory, and we must not leak it. Otherwise
-	 * we'd have problems e.g. when building indexes. So we create a local
-	 * memory context and make sure we free the memory before leaving this
-	 * function (not after every call).
-	 */
-	ctx = AllocSetContextCreate(CurrentMemoryContext,
-								"minmax-multi context",
-								ALLOCSET_DEFAULT_SIZES);
+	/* Reduce the ranges if needed */
+	if (ncranges > ranges->maxvalues)
+	{
+		/* and we'll also need the 'distance' procedure */
+		distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
 
-	oldctx = MemoryContextSwitchTo(ctx);
+		/*
+		 * The distanceFn calls (which may internally call e.g. numeric_le) may
+		 * allocate quite a bit of memory, and we must not leak it. Otherwise
+		 * we'd have problems e.g. when building indexes. So we create a local
+		 * memory context and make sure we free the memory before leaving this
+		 * function (not after every call).
+		 */
+		ctx = AllocSetContextCreate(CurrentMemoryContext,
+									"minmax-multi context",
+									ALLOCSET_DEFAULT_SIZES);
 
-	/* OK build the combine ranges */
-	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+		oldctx = MemoryContextSwitchTo(ctx);
 
-	/* build array of gap distances and sort them in ascending order */
-	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+		/* build array of gap distances and sort them in ascending order */
+		distances = build_distances(distanceFn, colloid, cranges, ncranges);
 
-	/*
-	 * Combine ranges until we release at least 25% of the space. This
-	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
-	 * use too low or high value.
-	 */
-	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
-									 ranges->maxvalues);
+		/*
+		 * Combine ranges until we release at least 25% of the space. This
+		 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+		 * use too low or high value.
+		 */
+		ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+										 ranges->maxvalues * MINMAX_LOAD_FACTOR,
+										 cmpFn, colloid);
 
-	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * 0.75);
+		Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR);
 
-	/* decompose the combine ranges into regular ranges and single values */
-	store_combine_ranges(ranges, cranges, ncranges);
+		/* decompose the combine ranges into regular ranges and single values */
+		store_combine_ranges(ranges, cranges, ncranges);
 
-	MemoryContextSwitchTo(oldctx);
-	MemoryContextDelete(ctx);
+		MemoryContextSwitchTo(oldctx);
+		MemoryContextDelete(ctx);
+	}
+	else
+		/*
+		 * This seems like a fairly expensive thing, maybe we could just
+		 * modify the ranges directly, instead of building cranges and
+		 * decomposing them again?
+		 */
+		store_combine_ranges(ranges, cranges, ncranges);
+
+	/* combine ranges were allocated outside the memory context */
+	pfree(cranges);
 
 	/* Check the ordering invariants are not violated (for both parts). */
 	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
@@ -1795,7 +1880,7 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 
 	/*
 	 * Try to add the new value to the range. We need to update the modified
-	 * flag, so that we serialize the correct value.
+	 * flag, so that we serialize the updated summary later.
 	 */
 	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
 
@@ -2066,19 +2151,28 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	/* check that the combine ranges are correct (no overlaps, ordering) */
 	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
 
-	/* build array of gap distances and sort them in ascending order */
-	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
-	distances = build_distances(distanceFn, colloid, cranges, ncranges);
-
 	/*
-	 * See how many values would be needed to store the current ranges, and if
-	 * needed combine as many off them to get below the maxvalues threshold.
-	 * The collapsed ranges will be stored as a single value.
-	 *
-	 * XXX The maxvalues may be different, so perhaps use Max?
+	 * If needed, reduce some of the ranges. This may be fairly expensive,
+	 * so only do that when necessary (when we have too many ranges).
 	 */
-	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
-									 ranges_a->maxvalues);
+	if (ncranges > ranges_a->maxvalues)
+	{
+		/* build array of gap distances and sort them in ascending order */
+		distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+		distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+		/*
+		 * See how many values would be needed to store the current ranges,
+		 * and if needed combine as many off them to get below the threshold.
+		 * The collapsed ranges will be stored as a single value.
+		 *
+		 * XXX Can the maxvalues be different in the two ranges? Perhaps
+		 * we should use maximum of those?
+		 */
+		ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+										 ranges_a->maxvalues,
+										 cmpFn, colloid);
+	}
 
 	/* update the first range summary */
 	store_combine_ranges(ranges_a, cranges, ncranges);
-- 
2.26.2

0008-batch-build-20210122.patchtext/x-patch; charset=UTF-8; name=0008-batch-build-20210122.patchDownload
From b4f05f6488e6e406254e70de594916f4821b0d3b Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Fri, 22 Jan 2021 01:11:08 +0100
Subject: [PATCH 8/8] batch build

---
 src/backend/access/brin/brin_minmax_multi.c | 204 ++++++++++++++++++--
 src/backend/access/brin/brin_tuple.c        |   1 +
 src/include/access/brin_tuple.h             |   2 +-
 3 files changed, 186 insertions(+), 21 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 094ecd2be9..72cca6f26b 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -57,6 +57,7 @@
 #include "access/brin.h"
 #include "access/brin_internal.h"
 #include "access/brin_tuple.h"
+#include "access/hash.h"	/* XXX strange that it fails because of BRIN_AM_OID without this */
 #include "access/reloptions.h"
 #include "access/stratnum.h"
 #include "access/htup_details.h"
@@ -150,12 +151,23 @@ typedef struct MinMaxOptions
 typedef struct Ranges
 {
 	Oid		typid;
+	Oid		colloid;
+	AttrNumber	attno;
 
 	/* (2*nranges + nvalues) <= maxvalues */
 	int		nranges;	/* number of ranges in the array (stored) */
 	int		nvalues;	/* number of values in the data array (all) */
 	int		maxvalues;	/* maximum number of values (reloption) */
 
+	/*
+	 * Batch mode means we're simply appending values into new range,
+	 * without any expensive steps (sorting, deduplication, ...). The
+	 * buffer is sized to be much larger, but we keep the actual target
+	 * size so that when serializing values we can do the right thing.
+	 */
+	bool	batch_mode;
+	int		target_maxvalues;
+
 	/* values stored for this range - either raw values, or ranges */
 	Datum	values[FLEXIBLE_ARRAY_MEMBER];
 } Ranges;
@@ -375,6 +387,7 @@ range_deserialize(SerializedRanges *serialized)
 	range->nvalues = serialized->nvalues;
 	range->maxvalues = serialized->maxvalues;
 	range->typid = serialized->typid;
+	range->batch_mode = false;
 
 	typbyval = get_typbyval(serialized->typid);
 	typlen = get_typlen(serialized->typid);
@@ -844,11 +857,14 @@ fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
 /*
  * Sort combine ranges using qsort (with BTLessStrategyNumber function).
  */
-static void
+static int
 sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
-					CombineRange *cranges, int ncranges)
+					CombineRange *cranges, int ncranges,
+					bool deduplicate)
 {
-	compare_context cxt;
+	int				n;
+	int				i;
+	compare_context	cxt;
 
 	/* sort the values */
 	cxt.colloid = colloid;
@@ -856,6 +872,24 @@ sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
 
 	qsort_arg(cranges, ncranges, sizeof(CombineRange),
 			  compare_combine_ranges, (void *) &cxt);
+
+	if (!deduplicate)
+		return ncranges;
+
+	/* optionally deduplicate the ranges */
+	n = 1;
+	for (i = 1; i < ncranges; i++)
+	{
+		if (compare_combine_ranges(&cranges[i-1], &cranges[i], (void *) &cxt))
+		{
+			if (i != n)
+				memcpy(&cranges[n], &cranges[i], sizeof(CombineRange));
+
+			n++;
+		}
+	}
+
+	return n;
 }
 
 /*
@@ -1026,26 +1060,40 @@ build_distances(FmgrInfo *distanceFn, Oid colloid,
  */
 static CombineRange *
 build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
-					 Datum newvalue, int *nranges)
+					 bool addvalue, Datum newvalue, int *nranges,
+					 bool deduplicate)
 {
 	int				ncranges;
 	CombineRange   *cranges;
 
 	/* now do the actual merge sort */
-	ncranges = ranges->nranges + ranges->nvalues + 1;
+	ncranges = ranges->nranges + ranges->nvalues;
+
+	/* should we add an extra value? */
+	if (addvalue)
+		ncranges += 1;
+
 	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
-	*nranges = ncranges;
 
 	/* put the new value at the beginning */
-	cranges[0].minval = newvalue;
-	cranges[0].maxval = newvalue;
-	cranges[0].collapsed = true;
+	if (addvalue)
+	{
+		cranges[0].minval = newvalue;
+		cranges[0].maxval = newvalue;
+		cranges[0].collapsed = true;
 
-	/* then the regular and collapsed ranges */
-	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+		/* then the regular and collapsed ranges */
+		fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+	}
+	else
+		fill_combine_ranges(cranges, ncranges, ranges);
 
 	/* and sort the ranges */
-	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+	ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges,
+								   deduplicate);
+
+	/* remember how many cranges we built */
+	*nranges = ncranges;
 
 	return cranges;
 }
@@ -1074,6 +1122,7 @@ count_values(CombineRange *cranges, int ncranges)
 }
 #endif
 
+
 /*
  * reduce_combine_ranges
  *		reduce the ranges until the number of values is low enough
@@ -1250,6 +1299,25 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 
 	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
 
+	Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0));
+
+	/*
+	 * When batch-building, there should be no ranges. So either the
+	 * number of ranges is 0 or we're not in batching mode.
+	 */
+	Assert(!ranges->batch_mode || (ranges->nranges == 0));
+
+	/* When batch-building, just add it and we're done. */
+	if (ranges->batch_mode)
+	{
+		/* there has to be free space, if we've sized the struct */
+		Assert(ranges->nvalues < ranges->maxvalues);
+
+		ranges->values[ranges->nvalues++] = newval;
+
+		return true;
+	}
+
 	/*
 	 * Bail out if the value already is covered by the range.
 	 *
@@ -1322,7 +1390,9 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 					 ranges->nvalues);
 
 	/* OK build the combine ranges */
-	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+	cranges = build_combine_ranges(cmpFn, colloid, ranges,
+								   true, newval, &ncranges,
+								   false);
 
 	/* Reduce the ranges if needed */
 	if (ncranges > ranges->maxvalues)
@@ -1382,6 +1452,81 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	return true;
 }
 
+/*
+ * Generate range representation of data collected during "batch mode".
+ * This is similar to reduce_combine_ranges, except that we can't assume
+ * the values are sorted and there may be duplicate values.
+ */
+static void
+compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	/*
+	 * This should only be used in batch mode, and there should be no
+	 * ranges, just individual values.
+	 */
+	Assert((ranges->batch_mode) && (ranges->nranges == 0));
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
+											   BTLessStrategyNumber);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, ranges->attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges,
+								   false, (Datum) 0, &ncranges,
+								   true);	/* deduplicate */
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, ranges->colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we get below max_values. We don't use any scale
+	 * factor, because this is used at the very end of "batch mode" and we
+	 * don't expect more tuples to be inserted soon.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									  max_values, cmpFn, ranges->colloid);
+
+	Assert(count_values(cranges, ncranges) <= max_values);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, ranges->colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, ranges->colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+}
+
 Datum
 brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 {
@@ -1799,10 +1944,16 @@ brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
 }
 
 static void
-brin_minmax_multi_serialize(Datum src, Datum *dst)
+brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
 {
 	Ranges *ranges = (Ranges *) DatumGetPointer(src);
-	SerializedRanges *s = range_serialize(ranges);
+	SerializedRanges *s;
+
+	/* In batch mode, we need to compress the accumulated values. */
+	if (ranges->batch_mode)
+		compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+
+	s = range_serialize(ranges);
 	dst[0] = PointerGetDatum(s);
 }
 
@@ -1845,15 +1996,28 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	/*
 	 * If this is the first non-null value, we need to initialize the range
 	 * list. Otherwise just extract the existing range list from BrinValues.
+	 *
+	 * When starting with an empty range, we assume this is a batch mode,
+	 * i.e. we size the buffer for the maximum possible number of items in
+	 * the range (based on range size and max number of items on a page).
+	 *
+	 * XXX This may require quite a bit of memory, so maybe we should use
+	 * some value in between. OTOH most tables will have much wider rows,
+	 * so the number of rows per page is much lower.
 	 */
 	if (column->bv_allnulls)
 	{
 		MemoryContext oldctx;
 
-		oldctx = MemoryContextSwitchTo(column->bv_context);
+		BlockNumber		pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
 
-		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+		ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange);
+		ranges->attno = attno;
+		ranges->colloid = colloid;
 		ranges->typid = attr->atttypid;
+		ranges->batch_mode = true;
+		ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
 
 		MemoryContextSwitchTo(oldctx);
 
@@ -2125,7 +2289,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	oldctx = MemoryContextSwitchTo(ctx);
 
 	/* allocate and fill */
-	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
 
 	/* fill the combine ranges with entries for the first range */
 	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
@@ -2139,8 +2303,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 											 BTLessStrategyNumber);
 
-	/* sort the combine ranges */
-	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+	/* sort the combine ranges (don't deduplicate) */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false);
 
 	/*
 	 * We've merged two different lists of ranges, so some of them may be
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index df820db029..bf8635d788 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -163,6 +163,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_serialize)
 		{
 			tuple->bt_columns[keyno].bv_serialize(
+				brdesc,
 				tuple->bt_columns[keyno].bv_mem_value,
 				tuple->bt_columns[keyno].bv_values);
 		}
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 66a6c3c792..e08998b71a 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,7 +14,7 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
-typedef void (*brin_serialize_callback_type) (Datum src, Datum * dst);
+typedef void (*brin_serialize_callback_type) (BrinDesc *bdesc, Datum src, Datum * dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
-- 
2.26.2

brin.shapplication/x-shellscript; name=brin.shDownload
#133John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#132)
Re: WIP: BRIN multi-range indexes

On Thu, Jan 21, 2021 at 9:06 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

[wip optimizations]

The pathological case (monotonic-asc) is now 4x faster, roughly equal to
regular minmax index build. The opposite (monotonic-desc) is about 33%
faster, roughly in line with btree.

Those numbers look good. I get similar results, shown below. I've read
0007-8 briefly but not in depth.

There are a couple cases where it's actually a bit slower - those are
the cases with very few distinct values per range. I believe this
happens because in the batch mode the code does not check if the summary
already contains this value, adds it to the buffer and the last step
ends up being more expensive than this.

I think if it's worst case a bit faster than btree and best case a bit
slower than traditional minmax, that's acceptable.

I believe there's some "compromise" between those two extremes, i.e. we
should use buffer that is too small or too large, but something in
between, so that the reduction happens once in a while but not too often
(as with the original aggressive approach).

This sounds good also.

FWIW, none of this is likely to be an issue in practice, because (a)
tables usually don't have such strictly monotonic patterns, (b) people
should stick to plain minmax for cases that do.

Still, it would be great if multi-minmax can be a drop in replacement. I
know there was a sticking point of a distance function not being available
on all types, but I wonder if that can be remedied or worked around somehow.

And (c) regular tables

tend to have much wider rows, so there are fewer values per range (so
that other stuff is likely more expensive than building BRIN).

True. I'm still puzzled that it didn't help to use 8 pages per range, but
it's moot now.

Here are some numbers (median of 3) with a similar scenario as before,
repeated here with some added details. I didn't bother with what you call
"unpatched":

btree minmax multi
monotonic-asc 44.4 26.5 27.8
mono-del-ins 38.7 24.6 30.4
mono-10-asc 61.8 25.6 33.5

create unlogged table iot (
id bigint generated by default as identity primary key,
num double precision not null,
create_dt timestamptz not null,
stuff text generated always as (md5(id::text)) stored
)
with (fillfactor = 95);

-- monotonic-asc:

insert into iot (num, create_dt)
select random(), x
from generate_series(
'2020-01-01 0:00'::timestamptz,
'2020-01-01 0:00'::timestamptz +'5 years'::interval,
'1 second'::interval) x;

-- mono-del-ins:
-- Here I deleted a few values from (likely) each page in the above table,
and reinserted values that shouldn't be in existing ranges:

delete from iot1
where num < 0.05
or num > 0.95;

vacuum iot1;

insert into iot (num, create_dt)
select random(), x
from generate_series(
'2020-01-01 0:00'::timestamptz,
'2020-02-01 23:59'::timestamptz,
'1 second'::interval) x;

-- mono-10-asc

truncate table iot;

insert into iot (num, create_dt)
select random(), '2020-01-01 0:00'::timestamptz + (x % 10 || '
seconds')::interval
from generate_series(1,5*365*24*60*60) x;

--
John Naylor
EDB: http://www.enterprisedb.com

#134Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#133)
Re: WIP: BRIN multi-range indexes

On 1/23/21 12:27 AM, John Naylor wrote:

On Thu, Jan 21, 2021 at 9:06 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

[wip optimizations]

The pathological case (monotonic-asc) is now 4x faster, roughly equal to
regular minmax index build. The opposite (monotonic-desc) is about 33%
faster, roughly in line with btree.

Those numbers look good. I get similar results, shown below. I've read
0007-8 briefly but not in depth.

There are a couple cases where it's actually a bit slower - those are

the cases with very few distinct values per range. I believe this
happens because in the batch mode the code does not check if the summary
already contains this value, adds it to the buffer and the last step
ends up being more expensive than this.

I think if it's worst case a bit faster than btree and best case a bit
slower than traditional minmax, that's acceptable.

I believe there's some "compromise" between those two extremes, i.e. we
should use buffer that is too small or too large, but something in
between, so that the reduction happens once in a while but not too often
(as with the original aggressive approach).

This sounds good also.

Yeah, I agree.

I think the reason why some of the cases got a bit slower is that in
those cases the original approach (ranges being built fairly frequently,
not just once at the end) we quickly built something that represented
the whole range, so adding a new value was often no-op. The add_value
callback found the range already "includes" the new value, etc.

With the batch mode, that's no longer true - we accumulate everything,
so we have to sort it etc. Which I guess may be fairly expensive, thanks
to calling comparator functions etc. I wonder if this could be optimized
a bit, e.g. by first "deduplicating" the values using memcmp() or so.

But ultimately, I think the right solution will be to limit the buffer
size to something like 10x the target, and roll with that. Typically,
increasing the buffer size from e.g. 100B to 1000B brings much clearer
improvement than increasing it from 1000B to 10000B. I'd bet this
follows the pattern.

FWIW, none of this is likely to be an issue in practice, because (a)
tables usually don't have such strictly monotonic patterns, (b) people
should stick to plain minmax for cases that do.

Still, it would be great if multi-minmax can be a drop in replacement. I
know there was a sticking point of a distance function not being
available on all types, but I wonder if that can be remedied or worked
around somehow.

Hmm. I think Alvaro also mentioned he'd like to use this as a drop-in
replacement for minmax (essentially, using these opclasses as the
default ones, with the option to switch back to plain minmax). I'm not
convinced we should do that - though. Imagine you have minmax indexes in
your existing DB, it's working perfectly fine, and then we come and just
silently change that during dump/restore. Is there some past example
when we did something similar and it turned it to be OK?

As for the distance functions, I'm pretty sure there are data types
without "natural" distance - like most strings, for example. We could
probably invent something, but the question is how much we can rely on
it working well enough in practice.

Of course, is minmax even the right index type for such data types?
Strings are usually "labels" and not queried using range queries,
although sometimes people encode stuff as strings (but then it's very
unlikely we'll define the distance definition well). So maybe for those
types a hash / bloom would be a better fit anyway.

But I do have an idea - maybe we can do without distances, in those
cases. Essentially, the primary issue of minmax indexes are outliers, so
what if we simply sort the values, keep one range in the middle and as
many single points on each tail?

Imagine we have N values, and we want to represent this by K values. We
simply sort the N values, keep (k-2)/2 values on each tail as outliers,
and use 2 values for the values in between.

Example:

input: [1, 2, 100, 110, 111, ..., 120, , ..., 130, 201, 202]

Given k = 6, we would keep 2 values on tails, and range for the rest:

[1, 2, (100, 130), 201, 202]

Of course, this does not optimize for the same thing as when we have
distance - in that case we try to minimize the "covering" of the input
data, something like

sum(length(r) for r in ranges) / (max(ranges) - min(ranges))

But maybe it's good enough when there's no distance function ...

And (c) regular tables

tend to have much wider rows, so there are fewer values per range (so
that other stuff is likely more expensive than building BRIN).

True. I'm still puzzled that it didn't help to use 8 pages per range,
but it's moot now.

I'd bet that even with just 8 pages, there were quite a few values in
the range - possibly hundreds per page. I haven't tried if the patches
help with smaller ranges, so maybe we should check.

Here are some numbers (median of 3) with a similar scenario as before,
repeated here with some added details. I didn't bother with what you
call "unpatched":

               btree   minmax   multi
monotonic-asc  44.4    26.5     27.8
mono-del-ins   38.7    24.6     30.4
mono-10-asc    61.8    25.6     33.5

Thanks. Those numbers seem reasonable.

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#135John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#127)
Re: WIP: BRIN multi-range indexes

Hi Tomas,
I took another look through the Bloom opclass portion (0004) with
sorted_mode omitted, and it looks good to me code-wise. I think this part
is close to commit-ready. I also did one more proofreading pass for
minor details.

+ rows per block). The default values is <literal>-0.1</literal>, and

+ greater than 0.0 and smaller than 1.0. The default values is 0.01,

s/values/value/

+ * bloom filter, we can easily and cheaply test wheter it contains values

s/wheter/whether/

+ * XXX We assume the bloom filters have the same parameters fow now. In the

s/fow/for/

+ * or null if it does not exists.

s/exists/exist/

+ * We do expect the bloom filter to eventually switch to hashing mode,
+ * and it's bound to be almost perfectly random, so not compressible.

Leftover from when it started out in sorted mode.

+ if ((m/8) > BLCKSZ)

It seems we need something more safe, to account for page header and tuple
header at least. As the comment before says, the filter will eventually not
be compressible. I remember we can't be exact here, with the possibility of
multiple columns, but we can leave a little slack space.

--
John Naylor
EDB: http://www.enterprisedb.com

#136John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#134)
Re: WIP: BRIN multi-range indexes

On Fri, Jan 22, 2021 at 10:59 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

On 1/23/21 12:27 AM, John Naylor wrote:

Still, it would be great if multi-minmax can be a drop in replacement. I
know there was a sticking point of a distance function not being
available on all types, but I wonder if that can be remedied or worked
around somehow.

Hmm. I think Alvaro also mentioned he'd like to use this as a drop-in
replacement for minmax (essentially, using these opclasses as the
default ones, with the option to switch back to plain minmax). I'm not
convinced we should do that - though. Imagine you have minmax indexes in
your existing DB, it's working perfectly fine, and then we come and just
silently change that during dump/restore. Is there some past example
when we did something similar and it turned it to be OK?

I was assuming pg_dump can be taught to insert explicit opclasses for
minmax indexes, so that upgrade would not cause surprises. If that's true,
only new indexes would have the different default opclass.

As for the distance functions, I'm pretty sure there are data types
without "natural" distance - like most strings, for example. We could
probably invent something, but the question is how much we can rely on
it working well enough in practice.

Of course, is minmax even the right index type for such data types?
Strings are usually "labels" and not queried using range queries,
although sometimes people encode stuff as strings (but then it's very
unlikely we'll define the distance definition well). So maybe for those
types a hash / bloom would be a better fit anyway.

Right.

But I do have an idea - maybe we can do without distances, in those
cases. Essentially, the primary issue of minmax indexes are outliers, so
what if we simply sort the values, keep one range in the middle and as
many single points on each tail?

That's an interesting idea. I think it would be a nice bonus to try to do
something along these lines. On the other hand, I'm not the one
volunteering to do the work, and the patch is useful as is.

--
John Naylor
EDB: http://www.enterprisedb.com

#137Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#136)
Re: WIP: BRIN multi-range indexes

On 1/26/21 7:52 PM, John Naylor wrote:

On Fri, Jan 22, 2021 at 10:59 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

On 1/23/21 12:27 AM, John Naylor wrote:

Still, it would be great if multi-minmax can be a drop in

replacement. I

know there was a sticking point of a distance function not being
available on all types, but I wonder if that can be remedied or worked
around somehow.

Hmm. I think Alvaro also mentioned he'd like to use this as a drop-in
replacement for minmax (essentially, using these opclasses as the
default ones, with the option to switch back to plain minmax). I'm not
convinced we should do that - though. Imagine you have minmax indexes in
your existing DB, it's working perfectly fine, and then we come and just
silently change that during dump/restore. Is there some past example
when we did something similar and it turned it to be OK?

I was assuming pg_dump can be taught to insert explicit opclasses for
minmax indexes, so that upgrade would not cause surprises. If that's
true, only new indexes would have the different default opclass.

Maybe, I suppose we could do that. But I always found such changes
happening silently in the background a bit suspicious, because it may be
quite confusing. I certainly wouldn't expect such difference between
creating a new index and index created by dump/restore. Did we do such
changes in the past? That might be a precedent, but I don't recall any
example ...

As for the distance functions, I'm pretty sure there are data types
without "natural" distance - like most strings, for example. We could
probably invent something, but the question is how much we can rely on
it working well enough in practice.

Of course, is minmax even the right index type for such data types?
Strings are usually "labels" and not queried using range queries,
although sometimes people encode stuff as strings (but then it's very
unlikely we'll define the distance definition well). So maybe for those
types a hash / bloom would be a better fit anyway.

Right.

But I do have an idea - maybe we can do without distances, in those
cases. Essentially, the primary issue of minmax indexes are outliers, so
what if we simply sort the values, keep one range in the middle and as
many single points on each tail?

That's an interesting idea. I think it would be a nice bonus to try to
do something along these lines. On the other hand, I'm not the one
volunteering to do the work, and the patch is useful as is.

IMO it's fairly small amount of code, so I'll take a stab at in in the
next version of the patch.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#138John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#137)
Re: WIP: BRIN multi-range indexes

On Tue, Jan 26, 2021 at 6:59 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

On 1/26/21 7:52 PM, John Naylor wrote:

On Fri, Jan 22, 2021 at 10:59 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

Hmm. I think Alvaro also mentioned he'd like to use this as a drop-in
replacement for minmax (essentially, using these opclasses as the
default ones, with the option to switch back to plain minmax). I'm

not

convinced we should do that - though. Imagine you have minmax

indexes in

your existing DB, it's working perfectly fine, and then we come and

just

silently change that during dump/restore. Is there some past example
when we did something similar and it turned it to be OK?

I was assuming pg_dump can be taught to insert explicit opclasses for
minmax indexes, so that upgrade would not cause surprises. If that's
true, only new indexes would have the different default opclass.

Maybe, I suppose we could do that. But I always found such changes
happening silently in the background a bit suspicious, because it may be
quite confusing. I certainly wouldn't expect such difference between
creating a new index and index created by dump/restore. Did we do such
changes in the past? That might be a precedent, but I don't recall any
example ...

I couldn't think of a comparable example either. It comes down to
evaluating risk. On the one hand it's nice if users get an enhancement
without having to know about it, on the other hand if there is some kind of
noticeable regression, that's bad.

--
John Naylor
EDB: http://www.enterprisedb.com

#139Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#135)
12 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

Here's an updated and significantly improved version of the patch
series, particularly the multi-minmax part. I've fixed a number of
stupid bugs in that, discovered by either valgrind or stress-tests.

I was surprised by some of the bugs, or rather that the existing
regression tests failed to crash on them, so it's probably worth briefly
discussing the details. There were two main classes of such bugs:

1) missing datumCopy

AFAICS this happened because there were a couple missing datumCopy
calls, and BRIN covers multiple pages, so with by-ref data types we
added a pointer but the buffer might have gone away unexpectedly.
Regular regression tests passed just fine, because brin_multi runs
almost separately, so the chance of the buffer being evicted was low.
Valgrind reported this (with a rather enigmatic message, as usual), and
so did a simple stress-test creating many indexes concurrently. Anything
causing aggressive eviction of buffer would do the trick, I think,
triggering segfaults, asserts, etc.

2) bogus opclass definitions

There were a couple opclasses referencing incorrect distance function,
intended for a different data type. I was scratching my head WTH the
regression tests pass, as there is a table to build multi-minmax index
on all supported data types. The reason is pretty silly - the table is
very small, just 100 rows, with very low fillfactor (so only couple
values per page), and the index was created with pages_per_range=1. So
the compaction was not triggered and the distance function was never
actually called. I've decided to build the indexes on a larger data set
first, to test this. But maybe this needs somewhat different approach.

BLOOM
-----

The attached patch series addresses comments from the last review. As
for the size limit, I've defined a new macro

#define BloomMaxFilterSize \
MAXALIGN_DOWN(BLCKSZ - \
(MAXALIGN(SizeOfPageHeaderData + \
sizeof(ItemIdData)) + \
MAXALIGN(sizeof(BrinSpecialSpace)) + \
SizeOfBrinTuple))

and use that to determine if the bloom filter is too large. IMO that's
close enough, considering that this is a best-effort check anyway (due
to not being able to consider multi-column indexes).

MINMAX-MULTI
------------

As mentioned, there's a lot of fixes and improvements in this part, but
the basic principle is still the same. I've kept it split into three
parts with different approaches to building, so that it's possible to do
benchmarks and comparisons, and pick the best one.

a) 0005 - Aggressively compacts the summary, by always keeping it within
the limit defined by values_per_range. So if the range contains more
values, this may trigger compaction very often in some cases (e.g. for
monotonic series).

One drawback is that the more often the compactions happen, the less
optimal the result is - the algorithm is kinda greedy, picking something
like local optimums in each step.

b) 0006 - Batch build, exactly the opposite of 0005. Accumulates all
values in a buffer, then does a single round of compaction at the very
end. This obviously doesn't have the "greediness" issues, but it may
consume quite a bit of memory for some data types and/or indexes with
large BRIN ranges.

c) 0007 - A hybrid approach, using a buffer that is multiple of the
user-specified value, with some safety min/max limits. IMO this is what
we should use, although perhaps with some tuning of the exact limits.

Attached is a spreadsheet with benchmark results for each of those three
approaches, on different data types (byval/byref), data set types, index
parameters (pages/values per range) etc. I think 0007 is a reasonable
compromise overall, with performance somewhere in betwen 0005 and 0006.
Of course, there are cases where it's somewhat slow, e.g. for data types
with expensive comparisons and data sets forcing frequent compactions,
in which case it's ~10x slower compared to regular minmax (in most cases
it's ~1.5x). Compared to btree, it's usually much faster - ~2-3x as fast
(except for some extreme cases, of course).

As for the opclasses for indexes without "natural" distance function,
implemented in 0008, I think we should drop it. In theory it works, but
I'm far from convinced it's actually useful in practice. Essentially, if
you have a data type with ordering but without a meaningful concept of a
distance, it's hard to say what is an outlier. I'd bet most of those
data types are used as "labels" where even the ordering is kinda
useless, i.e. hardly anyone uses range queries on things like names,
it's all just equality searches. Which means the bloom indexes are a
much better match for this use case.

The other thing we were considering was using the new multi-minmax
opclasses as default ones, replacing the existing minmax ones. IMHO we
shouldn't do that either. For existing minmax indexes that's useless
(the opclass seems to be working, otherwise the index would be dropped).
But even for new indexes I'm not sure it's the right thing, so I don't
plan to change this.

I'm also attaching the stress-test that I used to test the hell out of
this, creating indexes on various data sets, data types, with varying
index parameters, etc.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0009-Ignore-correlation-for-new-BRIN-opclasses-20210203.patchtext/x-patch; charset=UTF-8; name=0009-Ignore-correlation-for-new-BRIN-opclasses-20210203.patchDownload
From 79d87030f4b57712b24274c273d10950980db639 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 9/9] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 15a8258677..d2c4172f48 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -410,6 +410,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 02aa618b49..437efa8d31 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1798,6 +1798,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 47ca4ddbb5..bf40f9ce32 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index fdaff42722..5fbf8cf9c7 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0008-Define-multi-minmax-oclasses-for-types-with-20210203.patchtext/x-patch; charset=UTF-8; name=0008-Define-multi-minmax-oclasses-for-types-with-20210203.patchDownload
From 912158da9f585b4b7f24ca68ee211ec9d8c998e8 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Wed, 3 Feb 2021 19:12:05 +0100
Subject: [PATCH 8/9] Define multi-minmax oclasses for types without distance

The existing multi-minmax opclasses rely on "distance" function when
deciding how to build the ranges, covering all the values. In principle,
those opclasses try to minimize the lengths of ranges, i.e. maximize the
lengths of "gaps" between them.

For some data types it's hard to construct a distance function - types
like "text" or "name" generally serve as labels, and may have ordering
only. So this uses a different approach, based on the observation that
it's the outliers that "break" BRIN minmax indexes, i.e. the lowest and
highest values. So simply sort the values, keep the (K-2) extreme values
on either tail, and build a single range representing the values in the
middle. Of course, the question is whether this is actually useful, but
that's hard to judge, and it's the best we can do.
---
 src/backend/access/brin/brin_minmax_multi.c |  75 ++++++++++++
 src/include/catalog/pg_amop.dat             | 119 ++++++++++++++++++++
 src/include/catalog/pg_amproc.dat           | 113 +++++++++++++++++++
 src/include/catalog/pg_opclass.dat          |  21 ++++
 src/include/catalog/pg_opfamily.dat         |  14 +++
 5 files changed, 342 insertions(+)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 90d17e5008..02aa618b49 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1210,6 +1210,9 @@ build_distances(FmgrInfo *distanceFn, Oid colloid,
 
 	Assert(ncranges >= 2);
 
+	if (!distanceFn)
+		return NULL;
+
 	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
 
 	/*
@@ -1298,6 +1301,70 @@ count_values(CombineRange *cranges, int ncranges)
 }
 #endif
 
+
+static int
+reduce_combine_ranges_simple(CombineRange *cranges, int ncranges,
+							 int max_values, FmgrInfo *cmp, Oid colloid)
+{
+	int		i;
+	int		nvalues;
+	Datum  *values;
+
+	compare_context cxt;
+
+	/* number of values to keep on each tail */
+	int tail = (max_values - 2) / 2;
+	int m = Min(tail / 2, ncranges - 1 / 2);
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	for (i = 0; i < m; i++)
+	{
+		/* head */
+		values[nvalues++] = cranges[i].minval;
+		values[nvalues++] = cranges[i].maxval;
+
+		/* tail */
+		values[nvalues++] = cranges[ncranges - 1 - i].minval;
+		values[nvalues++] = cranges[ncranges - 1 - i].maxval;
+	}
+
+	/* middle part */
+	values[nvalues++] = cranges[m].maxval;
+	values[nvalues++] = cranges[ncranges - 1 - m].minval;
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/*
+	 * Sort the values using the comparator function, and form ranges
+	 * from the sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		cranges[i].minval = values[2*i];
+		cranges[i].maxval = values[2*i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		cranges[i].collapsed = (compare_values(&values[2*i],
+											   &values[2*i+1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+
 /*
  * reduce_combine_ranges
  *		reduce the ranges until the number of values is low enough
@@ -1366,6 +1433,14 @@ reduce_combine_ranges(CombineRange *cranges, int ncranges,
 	if (keep >= ndistances)
 		return ncranges;
 
+	/*
+	 * Without distances, we use a simple approach keeping as many
+	 * outliers as possible.
+	 */
+	if (!distances)
+		return reduce_combine_ranges_simple(cranges, ncranges, max_values,
+											cmp, colloid);
+
 	/* sort the values */
 	cxt.colloid = colloid;
 	cxt.cmpFn = cmp;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 762ac3acef..4ab4aff073 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,23 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# minmax multi bytea
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '<(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '2', amopopr => '<=(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '3', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '4', amopopr => '>=(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # bloom bytea
 { amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
@@ -1836,6 +1853,23 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# minmax multi "char"
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '2', amopopr => '<=(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '3', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '4', amopopr => '>=(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
+  amopmethod => 'brin' },
+
 # bloom "char"
 { amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
@@ -1858,6 +1892,23 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# minmax multi name
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '2', amopopr => '<=(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '3', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '4', amopopr => '>=(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
+  amopmethod => 'brin' },
+
 # bloom name
 { amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
@@ -2210,6 +2261,23 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# minmax multi text
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '2', amopopr => '<=(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '3', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '4', amopopr => '>=(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
+  amopmethod => 'brin' },
+
 # bloom text
 { amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
@@ -2592,6 +2660,23 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# minmax multi character
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '<(bpchar,bpchar)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '2',
+  amopopr => '<=(bpchar,bpchar)', amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '3', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '4',
+  amopopr => '>=(bpchar,bpchar)', amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # bloom character
 { amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
   amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
@@ -3061,6 +3146,23 @@
   amoprighttype => 'bit', amopstrategy => '5', amopopr => '>(bit,bit)',
   amopmethod => 'brin' },
 
+# minmax multi bit
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '2', amopopr => '<=(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '3', amopopr => '=(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '4', amopopr => '>=(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '5', amopopr => '>(bit,bit)',
+  amopmethod => 'brin' },
+
 # minmax bit varying
 { amopfamily => 'brin/varbit_minmax_ops', amoplefttype => 'varbit',
   amoprighttype => 'varbit', amopstrategy => '1', amopopr => '<(varbit,varbit)',
@@ -3078,6 +3180,23 @@
   amoprighttype => 'varbit', amopstrategy => '5', amopopr => '>(varbit,varbit)',
   amopmethod => 'brin' },
 
+# minmax multi bit varying
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '1', amopopr => '<(varbit,varbit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '2',
+  amopopr => '<=(varbit,varbit)', amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '3', amopopr => '=(varbit,varbit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '4',
+  amopopr => '>=(varbit,varbit)', amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '5', amopopr => '>(varbit,varbit)',
+  amopmethod => 'brin' },
+
 # minmax numeric
 { amopfamily => 'brin/numeric_minmax_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 51403716b1..69a7ed682c 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,22 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi bytea
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom bytea
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '1',
@@ -836,6 +852,22 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi "char"
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom "char"
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -867,6 +899,22 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi name
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom name
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -1197,6 +1245,22 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi text
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom text
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -1663,6 +1727,23 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi character
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom character
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '1',
@@ -2180,6 +2261,21 @@
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi bit
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # minmax bit varying
 { amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
   amprocrighttype => 'varbit', amprocnum => '1',
@@ -2194,6 +2290,23 @@
   amprocrighttype => 'varbit', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi bit varying
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # minmax numeric
 { amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index da25befefe..823f1b01fe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,18 +266,27 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_minmax_multi_ops',
+  opcfamily => 'brin/bytea_minmax_multi_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bytea_bloom_ops',
   opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
   opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_minmax_multi_ops',
+  opcfamily => 'brin/char_minmax_multi_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_bloom_ops',
   opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
   opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_minmax_multi_ops',
+  opcfamily => 'brin/name_minmax_multi_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_bloom_ops',
   opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
   opckeytype => 'name', opcdefault => 'f' },
@@ -311,6 +320,9 @@
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_minmax_multi_ops',
+  opcfamily => 'brin/text_minmax_multi_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_bloom_ops',
   opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
   opckeytype => 'text', opcdefault => 'f' },
@@ -381,6 +393,9 @@
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_minmax_multi_ops',
+  opcfamily => 'brin/bpchar_minmax_multi_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
   opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar', opcdefault => 'f' },
@@ -440,9 +455,15 @@
   opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
+{ opcmethod => 'brin', opcname => 'bit_minmax_multi_ops',
+  opcfamily => 'brin/bit_minmax_multi_ops', opcintype => 'bit',
+  opckeytype => 'bit', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
   opcfamily => 'brin/varbit_minmax_ops', opcintype => 'varbit',
   opckeytype => 'varbit' },
+{ opcmethod => 'brin', opcname => 'varbit_minmax_multi_ops',
+  opcfamily => 'brin/varbit_minmax_multi_ops', opcintype => 'varbit',
+  opckeytype => 'varbit', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index ba9231ac8c..ffb20e72ab 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -192,6 +192,8 @@
   opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '9967',
+  opfmethod => 'brin', opfname => 'text_minmax_multi_ops' },
 { oid => '9902',
   opfmethod => 'brin', opfname => 'text_bloom_ops' },
 { oid => '9903',
@@ -210,14 +212,20 @@
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '9968',
+  opfmethod => 'brin', opfname => 'char_minmax_multi_ops' },
 { oid => '9906',
   opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '9969',
+  opfmethod => 'brin', opfname => 'bytea_minmax_multi_ops' },
 { oid => '9907',
   opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '9970',
+  opfmethod => 'brin', opfname => 'name_minmax_multi_ops' },
 { oid => '9908',
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
@@ -260,6 +268,8 @@
   opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '9971',
+  opfmethod => 'brin', opfname => 'bpchar_minmax_multi_ops' },
 { oid => '9915',
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
@@ -276,8 +286,12 @@
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
+{ oid => '9972',
+  opfmethod => 'brin', opfname => 'bit_minmax_multi_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
+{ oid => '9973',
+  opfmethod => 'brin', opfname => 'varbit_minmax_multi_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
 { oid => '9938',
-- 
2.26.2

0007-Remove-the-special-batch-mode-use-a-larger--20210203.patchtext/x-patch; charset=UTF-8; name=0007-Remove-the-special-batch-mode-use-a-larger--20210203.patchDownload
From 6ce3e9b887578021f77bb9003ad34877170bf6d5 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Tue, 2 Feb 2021 01:57:54 +0100
Subject: [PATCH 7/9] Remove the special batch mode, use a larger buffer always

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-specified size). That should give
us most of the benefits without consuming a lot of memory.
---
 src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++--------
 1 file changed, 525 insertions(+), 326 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index d1eafa9700..90d17e5008 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -92,7 +92,15 @@
  */
 #define		PROCNUM_BASE			11
 
-#define		MINMAX_LOAD_FACTOR		0.75
+/*
+ * Sizing the insert buffer - we use 10x the number of values specified
+ * in the reloption, but we cap it to 8192 not to get too large. When
+ * the buffer gets full, we reduce the number of values by half.
+ */
+#define		MINMAX_BUFFER_FACTOR			10
+#define		MINMAX_BUFFER_MIN				256
+#define		MINMAX_BUFFER_MAX				8192
+#define		MINMAX_BUFFER_LOAD_FACTOR		0.5
 
 typedef struct MinmaxMultiOpaque
 {
@@ -155,23 +163,24 @@ typedef struct Ranges
 	Oid			typid;
 	Oid			colloid;
 	AttrNumber	attno;
+	FmgrInfo   *cmp;
 
 	/* (2*nranges + nvalues) <= maxvalues */
 	int		nranges;	/* number of ranges in the array (stored) */
+	int		nsorted;	/* number of sorted values (ranges + points) */
 	int		nvalues;	/* number of values in the data array (all) */
 	int		maxvalues;	/* maximum number of values (reloption) */
 
 	/*
-	 * In batch mode, we simply add the values into a buffer, without any
-	 * expensive steps (sorting, deduplication, ...). The buffer is sized
-	 * to be larger than the target number of values per range, which
-	 * reduces the number of compactions - operating on larger buffers is
-	 * significantly more efficient, in most cases. We keep the actual
-	 * target and compact to the requested number of values at the very
-	 * end, before serializing to on-disk representation.
+	 * We simply add the values into a large buffer, without any expensive
+	 * steps (sorting, deduplication, ...). The buffer is a multiple of
+	 * the target number of values, so the compaction happen less often,
+	 * amortizing the costs. We keep the actual target and compact to
+	 * the requested number of values at the very end, before serializing
+	 * to on-disk representation.
 	 */
-	bool	batch_mode;
-	int		target_maxvalues;	/* requested number of values */
+	/* requested number of values */
+	int		target_maxvalues;
 
 	/* values stored for this range - either raw values, or ranges */
 	Datum	values[FLEXIBLE_ARRAY_MEMBER];
@@ -203,7 +212,7 @@ typedef struct SerializedRanges
 
 static SerializedRanges *range_serialize(Ranges *range);
 
-static Ranges *range_deserialize(SerializedRanges *range);
+static Ranges *range_deserialize(int maxvalues, SerializedRanges *range);
 
 /* Cache for support and strategy procesures. */
 
@@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
 static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
 					   uint16 attno, Oid subtype, uint16 strategynum);
 
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+static int compare_values(const void *a, const void *b, void *arg);
+
 
 /*
  * minmax_multi_init
@@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues)
 	return ranges;
 }
 
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid);
+
+
+static void
+range_deduplicate_values(Ranges *range)
+{
+	int				i, n;
+	int				start;
+	compare_context cxt;
+
+	/*
+	 * If there are no unsorted values, we're done (this probably can't
+	 * happen, as we're adding values to unsorted part).
+	 */
+	if (range->nsorted == range->nvalues)
+		return;
+
+	/* sort the values */
+	cxt.colloid = range->colloid;
+	cxt.cmpFn = range->cmp;
+
+	/* how many values to sort? */
+	start = 2 * range->nranges;
+
+	qsort_arg(&range->values[start],
+			  range->nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	n = 1;
+	for (i = 1; i < range->nvalues; i++)
+	{
+		/* same as preceding value, so store it */
+		if (compare_values(&range->values[start + i - 1],
+						   &range->values[start + i],
+						   (void *) &cxt) == 0)
+			continue;
+
+		range->values[start + n] = range->values[start + i];
+
+		n++;
+	}
+
+	/* now all the values are sorted */
+	range->nvalues = n;
+	range->nsorted = n;
+
+	AssertCheckRanges(range, range->cmp, range->colloid);
+}
+
+
 /*
  * range_serialize
  *	  Serialize the in-memory representation into a compact varlena value.
@@ -262,14 +330,25 @@ range_serialize(Ranges *range)
 
 	/* simple sanity checks */
 	Assert(range->nranges >= 0);
+	Assert(range->nsorted >= 0);
 	Assert(range->nvalues >= 0);
 	Assert(range->maxvalues > 0);
+	Assert(range->target_maxvalues > 0);
+
+	/* at this point the range should be compacted to the target size */
+	Assert(2*range->nranges + range->nvalues <= range->target_maxvalues);
+
+	Assert(range->target_maxvalues <= range->maxvalues);
+
+	/* range boundaries are always sorted */
+	Assert(range->nvalues >= range->nsorted);
+
+	/* sort and deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
 
 	/* see how many Datum values we actually have */
 	nvalues = 2*range->nranges + range->nvalues;
 
-	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
-
 	typid = range->typid;
 	typbyval = get_typbyval(typid);
 	typlen = get_typlen(typid);
@@ -316,7 +395,7 @@ range_serialize(Ranges *range)
 	serialized->typid = typid;
 	serialized->nranges = range->nranges;
 	serialized->nvalues = range->nvalues;
-	serialized->maxvalues = range->maxvalues;
+	serialized->maxvalues = range->target_maxvalues;
 
 	/*
 	 * And now copy also the boundary values (like the length calculation
@@ -367,7 +446,7 @@ range_serialize(Ranges *range)
  * in the in-memory value array.
  */
 static Ranges *
-range_deserialize(SerializedRanges *serialized)
+range_deserialize(int maxvalues, SerializedRanges *serialized)
 {
 	int		i,
 			nvalues;
@@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized)
 	nvalues = 2*serialized->nranges + serialized->nvalues;
 
 	Assert(nvalues <= serialized->maxvalues);
+	Assert(serialized->maxvalues <= maxvalues);
 
-	range = minmax_multi_init(serialized->maxvalues);
+	range = minmax_multi_init(maxvalues);
 
 	/* copy the header info */
 	range->nranges = serialized->nranges;
 	range->nvalues = serialized->nvalues;
-	range->maxvalues = serialized->maxvalues;
+	range->nsorted = serialized->nvalues;
+	range->maxvalues = maxvalues;
+	range->target_maxvalues = serialized->maxvalues;
+
 	range->typid = serialized->typid;
-	range->batch_mode = false;
 
 	typbyval = get_typbyval(serialized->typid);
 	typlen = get_typlen(serialized->typid);
@@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized)
 	return range;
 }
 
-typedef struct compare_context
-{
-	FmgrInfo   *cmpFn;
-	Oid			colloid;
-} compare_context;
-
 /*
  * Used to represent ranges expanded during merging and combining (to
  * reduce number of boundary values to store).
@@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg)
 	return 0;
 }
 
+void *bsearch_arg(const void *key, const void *base,
+						 size_t nmemb, size_t size,
+						 int (*compar) (const void *, const void *, void *),
+						 void *arg);
+
+static bool
+has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges,
+				   Datum newval, AttrNumber attno, Oid typid)
+{
+	Datum	compar;
+
+	Datum	minvalue = ranges->values[0];
+	Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+	FmgrInfo *cmpLessFn;
+	FmgrInfo *cmpGreaterFn;
+
+	/* binary search on ranges */
+	int		start,
+			end;
+
+	if (ranges->nranges == 0)
+		return false;
+
+	/*
+	 * Otherwise, need to compare the new value with boundaries of all
+	 * the ranges. First check if it's less than the absolute minimum,
+	 * which is the first value in the array.
+	 */
+	cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+										 BTLessStrategyNumber);
+	compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+	/* smaller than the smallest value in the range list */
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * And now compare it to the existing maximum (last value in the
+	 * data array). But only if we haven't already ruled out a possible
+	 * match in the minvalue check.
+	 */
+	cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+										BTGreaterStrategyNumber);
+	compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * So we know it's in the general min/max, the question is whether it
+	 * falls in one of the ranges or gaps. We'll use a binary search on
+	 * the ranges.
+	 *
+	 * it's in the general range, but is it actually covered by any
+	 * of the ranges? Repeat the check for each range.
+	 *
+	 * XXX We simply walk the ranges sequentially, but maybe we could
+	 * further leverage the ordering and non-overlap and use bsearch to
+	 * speed this up a bit.
+	 */
+	start = 0;					/* first range */
+	end = ranges->nranges - 1;	/* last range */
+	while (true)
+	{
+		int		midpoint = (start + end) / 2;
+
+		/* this means we ran out of ranges in the last step */
+		if (start > end)
+			return false;
+
+		/* copy the min/max values from the ranges */
+		minvalue = ranges->values[2 * midpoint];
+		maxvalue = ranges->values[2 * midpoint + 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse
+		 * to the left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in this range */
+		if (DatumGetBool(compar))
+		{
+			end = (midpoint - 1);
+			continue;
+		}
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse
+		 * to the right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+		/* larger than the largest value in this range */
+		if (DatumGetBool(compar))
+		{
+			start = (midpoint + 1);
+			continue;
+		}
+
+		/* hey, we found a matching range */
+		return true;
+	}
+
+	return false;
+}
+
+
 /*
  * range_contains_value
  * 		See if the new value is already contained in the range list.
@@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid,
 							Ranges *ranges, Datum newval)
 {
 	int			i;
-	FmgrInfo   *cmpLessFn;
-	FmgrInfo   *cmpGreaterFn;
 	FmgrInfo   *cmpEqualFn;
 	Oid			typid = attr->atttypid;
 
@@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid,
 	 * range, and only when there's still a chance of getting a match we
 	 * inspect the individual ranges.
 	 */
-	if (ranges->nranges > 0)
-	{
-		Datum	compar;
-		bool	match = true;
-
-		Datum	minvalue = ranges->values[0];
-		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
-
-		/*
-		 * Otherwise, need to compare the new value with boundaries of all
-		 * the ranges. First check if it's less than the absolute minimum,
-		 * which is the first value in the array.
-		 */
-		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
-											 BTLessStrategyNumber);
-		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
-
-		/* smaller than the smallest value in the range list */
-		if (DatumGetBool(compar))
-			match = false;
-
-		/*
-		 * And now compare it to the existing maximum (last value in the
-		 * data array). But only if we haven't already ruled out a possible
-		 * match in the minvalue check.
-		 */
-		if (match)
-		{
-			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
-												BTGreaterStrategyNumber);
-			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
-
-			if (DatumGetBool(compar))
-				match = false;
-		}
-
-		/*
-		 * So it's in the general range, but is it actually covered by any
-		 * of the ranges? Repeat the check for each range.
-		 *
-		 * XXX We simply walk the ranges sequentially, but maybe we could
-		 * further leverage the ordering and non-overlap and use bsearch to
-		 * speed this up a bit.
-		 */
-		for (i = 0; i < ranges->nranges && match; i++)
-		{
-			/* copy the min/max values from the ranges */
-			minvalue = ranges->values[2*i];
-			maxvalue = ranges->values[2*i+1];
-
-			/*
-			 * Otherwise, need to compare the new value with boundaries of all
-			 * the ranges. First check if it's less than the absolute minimum,
-			 * which is the first value in the array.
-			 */
-			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
-
-			/* smaller than the smallest value in this range */
-			if (DatumGetBool(compar))
-				continue;
-
-			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
-
-			/* larger than the largest value in this range */
-			if (DatumGetBool(compar))
-				continue;
-
-			/* hey, we found a matching row */
-			return true;
-		}
-	}
+	if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid))
+		return true;
 
 	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
 											 BTEqualStrategyNumber);
@@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid,
 	/*
 	 * We're done with the ranges, now let's inspect the exact values.
 	 *
-	 * XXX Again, we do sequentially search the values - consider leveraging
-	 * the ordering of values to improve performance.
+	 * XXX We do sequential search for small number of values, and bsearch
+	 * once we have more than 16 values.
+	 *
+	 * XXX We only inspect the sorted part - that's OK. For building it may
+	 * produce false negatives, but only after we already added some values
+	 * to the unsorted part, so we've modified the value. And when querying
+	 * the index, there should be no unsorted values.
 	 */
-	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	if (ranges->nsorted >= 16)
 	{
-		Datum compar;
+		compare_context	cxt;
 
-		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
 
-		/* found an exact match */
-		if (DatumGetBool(compar))
+		if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) != NULL)
 			return true;
 	}
-
-	/* the value is not covered by this BRIN tuple */
-	return false;
-}
-
-/*
- * insert_value
- *	  Adds a new value into the single-point part, while maintaining ordering.
- *
- * The function inserts the new value to the right place in the single-point
- * part of the range. It assumes there's enough free space, and then does
- * essentially an insert-sort.
- *
- * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
- * only the first nvalues are used.
- */
-static void
-insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
-			 Datum newvalue)
-{
-	int	i;
-	Datum	lt;
-
-	/* If there are no values yet, store the new one and we're done. */
-	if (!nvalues)
+	else
 	{
-		values[0] = newvalue;
-		return;
-	}
-
-	/*
-	 * A common case is that the new value is entirely out of the existing
-	 * range, i.e. it's either smaller or larger than all previous values.
-	 * So we check and handle this case first - first we check the larger
-	 * case, because in that case we can just append the value to the end
-	 * of the array and we're done.
-	 */
+		for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++)
+		{
+			Datum compar;
 
-	/* Is it greater than all existing values in the array? */
-	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
-	if (DatumGetBool(lt))
-	{
-		/* just copy it in-place and we're done */
-		values[nvalues] = newvalue;
-		return;
-	}
+			compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
 
-	/*
-	 * OK, I lied a bit - we won't check the smaller case explicitly, but
-	 * we'll just compare the value to all existing values in the array.
-	 * But we happen to start with the smallest value, so we're actually
-	 * doing the check anyway.
-	 *
-	 * XXX We do walk the values sequentially. Perhaps we could/should be
-	 * smarter and do some sort of bisection, to improve performance?
-	 */
-	for (i = 0; i < nvalues; i++)
-	{
-		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
-		if (DatumGetBool(lt))
-		{
-			/*
-			 * Move values to make space for the new entry, which should go
-			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
-			 */
-			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
-			values[i] = newvalue;
-			return;
+			/* found an exact match */
+			if (DatumGetBool(compar))
+				return true;
 		}
 	}
 
-	/* We should never really get here. */
-	Assert(false);
+	/* the value is not covered by this BRIN tuple */
+	return false;
 }
 
 #ifdef USE_ASSERT_CHECKING
@@ -754,11 +818,12 @@ static void
 AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
 {
 #ifdef USE_ASSERT_CHECKING
-	int i, j;
+	int i;
 
 	/* some basic sanity checks */
 	Assert(ranges->nranges >= 0);
-	Assert(ranges->nvalues >= 0);
+	Assert(ranges->nsorted >= 0);
+	Assert(ranges->nvalues >= ranges->nsorted);
 	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
 	Assert(ranges->typid != InvalidOid);
 
@@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
 	 */
 	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
 
-	/* finally check that none of the values are not covered by ranges */
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nsorted);
+
+	/*
+	 * Check that none of the values are not covered by ranges (both
+	 * sorted and unsorted)
+	 */
 	for (i = 0; i < ranges->nvalues; i++)
 	{
+		Datum	compar;
+		int		start,
+				end;
+		Datum	minvalue,
+				maxvalue;
+
 		Datum	value = ranges->values[2 * ranges->nranges + i];
 
-		for (j = 0; j < ranges->nranges; j++)
+		if (ranges->nranges == 0)
+			break;
+
+		minvalue = ranges->values[0];
+		maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse
+		 * to the left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+		/* smaller than the smallest value in the first range */
+		if (DatumGetBool(compar))
+			continue;
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse
+		 * to the right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+		/* larger than the largest value in the last range */
+		if (DatumGetBool(compar))
+			continue;
+
+		start = 0;					/* first range */
+		end = ranges->nranges - 1;	/* last range */
+		while (true)
 		{
-			Datum	r;
+			int		midpoint = (start + end) / 2;
+
+			/* this means we ran out of ranges in the last step */
+			if (start > end)
+				break;
+
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2 * midpoint];
+			maxvalue = ranges->values[2 * midpoint + 1];
 
-			Datum	minval = ranges->values[2 * j];
-			Datum	maxval = ranges->values[2 * j + 1];
+			/*
+			 * Is the value smaller than the minval? If yes, we'll recurse
+			 * to the left side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
 
-			/* if value is smaller than range minimum, that's OK */
-			r = FunctionCall2Coll(cmpFn, colloid, value, minval);
-			if (DatumGetBool(r))
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+			{
+				end = (midpoint - 1);
 				continue;
+			}
+
+			/*
+			 * Is the value greater than the minval? If yes, we'll recurse
+			 * to the right side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
 
-			/* if value is greater than range maximum, that's OK */
-			r = FunctionCall2Coll(cmpFn, colloid, maxval, value);
-			if (DatumGetBool(r))
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+			{
+				start = (midpoint + 1);
 				continue;
+			}
 
-			/* value is between [min,max], which is wrong */
+			/* hey, we found a matching range */
 			Assert(false);
 		}
 	}
+
+	/* and values in the unsorted part must not be in sorted part */
+	for (i = ranges->nsorted; i < ranges->nvalues; i++)
+	{
+		compare_context	cxt;
+		Datum	value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nsorted == 0)
+			break;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) == NULL);
+	}
 #endif
 }
 
@@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid,
  */
 static CombineRange *
 build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
-					 bool addvalue, Datum newvalue, int *nranges,
-					 bool deduplicate)
+					 int *nranges)
 {
 	int				ncranges;
 	CombineRange   *cranges;
@@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
 	/* now do the actual merge sort */
 	ncranges = ranges->nranges + ranges->nvalues;
 
-	/* should we add an extra value? */
-	if (addvalue)
-		ncranges += 1;
-
 	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
 
-	/* put the new value at the beginning */
-	if (addvalue)
-	{
-		cranges[0].minval = newvalue;
-		cranges[0].maxval = newvalue;
-		cranges[0].collapsed = true;
-
-		/* then the regular and collapsed ranges */
-		fill_combine_ranges(&cranges[1], ncranges-1, ranges);
-	}
-	else
-		fill_combine_ranges(cranges, ncranges, ranges);
+	/* fll the combine ranges */
+	fill_combine_ranges(cranges, ncranges, ranges);
 
 	/* and sort the ranges */
-	ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges,
-								   deduplicate);
+	ncranges = sort_combine_ranges(cmp, colloid,
+								   cranges, ncranges,
+								   true);	/* deduplicate */
 
 	/* remember how many cranges we built */
 	*nranges = ncranges;
@@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
 		}
 	}
 
+	/* all the values are sorted */
+	ranges->nsorted = ranges->nvalues;
+
 	Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues);
 	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
 }
 
+
+
 /*
- * range_add_value
- * 		Add the new value to the multi-minmax range.
+ * Consider freeing space in the ranges.
+ *
+ * Returns true if the value was actually modified.
  */
 static bool
-range_add_value(BrinDesc *bdesc, Oid colloid,
-				AttrNumber attno, Form_pg_attribute attr,
-				Ranges *ranges, Datum newval)
+ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *range)
 {
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
 	FmgrInfo   *cmpFn,
 			   *distanceFn;
 
@@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	int				ncranges;
 	DistanceValue  *distances;
 
-	MemoryContext	ctx;
-	MemoryContext	oldctx;
-
-	/* we'll certainly need the comparator, so just look it up now */
-	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
-											   BTLessStrategyNumber);
-
-	/* comprehensive checks of the input ranges */
-	AssertCheckRanges(ranges, cmpFn, colloid);
-
-	Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0));
-
 	/*
-	 * When batch-building, there should be no ranges. So either the
-	 * number of ranges is 0 or we're not in batching mode.
+	 * If there is free space in the buffer, we're done without having
+	 * to modify anything.
 	 */
-	Assert(!ranges->batch_mode || (ranges->nranges == 0));
-
-	/* When batch-building, just add it and we're done. */
-	if (ranges->batch_mode)
-	{
-		/* there has to be free space, if we've sized the struct */
-		Assert(ranges->nvalues < ranges->maxvalues);
-
-		/* Make a copy of the value, if needed. */
-		ranges->values[ranges->nvalues++]
-			= datumCopy(newval, attr->attbyval, attr->attlen);;
-
-		return true;
-	}
-
-	/*
-	 * Bail out if the value already is covered by the range.
-	 *
-	 * We could also add values until we hit values_per_range, and then
-	 * do the deduplication in a batch, hoping for better efficiency. But
-	 * that would mean we actually modify the range every time, which means
-	 * having to serialize the value, which does palloc, walks the values,
-	 * copies them, etc. Not exactly cheap.
-	 *
-	 * So instead we do the check, which should be fairly cheap - assuming
-	 * the comparator function is not very expensive.
-	 *
-	 * This also implies means the values array can't contain duplicities.
-	 */
-	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+	if (2*range->nranges + range->nvalues < range->maxvalues)
 		return false;
 
-	/* Make a copy of the value, if needed. */
-	newval = datumCopy(newval, attr->attbyval, attr->attlen);
-
-	/*
-	 * If there's space in the values array, copy it in and we're done.
-	 *
-	 * We do want to keep the values sorted (to speed up searches), so we
-	 * do a simple insertion sort. We could do something more elaborate,
-	 * e.g. by sorting the values only now and then, but for small counts
-	 * (e.g. when maxvalues is 64) this should be fine.
-	 */
-	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
-	{
-		Datum	   *values;
-
-		/* beginning of the 'single value' part (for convenience) */
-		values = &ranges->values[2*ranges->nranges];
-
-		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
-
-		ranges->nvalues++;
-
-		/*
-		 * Check we haven't broken the ordering of boundary values (checks
-		 * both parts, but that doesn't hurt).
-		 */
-		AssertCheckRanges(ranges, cmpFn, colloid);
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
 
-		/* Also check the range contains the value we just added. */
-		// FIXME Assert(ranges, cmpFn, colloid);
+	/* Try deduplicating values in the unsorted part */
+	range_deduplicate_values(range);
 
-		/* yep, we've modified the range */
+	/* did we reduce enough free space by just the deduplication? */
+	if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR)
 		return true;
-	}
 
 	/*
-	 * Damn - the new value is not in the range yet, but we don't have space
-	 * to just insert it. So we need to combine some of the existing ranges,
-	 * to reduce the number of values we need to store (joining two intervals
-	 * reduces the number of boundaries to store by 2).
+	 * we need to combine some of the existing ranges, to reduce the number
+	 * of values we need to store (joining intervals reduces the number of
+	 * boundary values).
 	 *
-	 * To do that we first construct an array of CombineRange items - each
-	 * combine range tracks if it's a regular range or collapsed range, where
-	 * "collapsed" means "single point."
+	 * We first construct an array of CombineRange items - each combine range
+	 * tracks if it's a regular range or a collapsed range, where "collapsed"
+	 * means "single point." This makes the processing easier, as it allows
+	 * handling ranges and points the same way.
 	 *
-	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
-	 * directly, while single points (ranges->nvalues of them) have to be
-	 * expanded. We neet the combine ranges to be sorted, and we do that by
-	 * performing a merge sort of ranges, values and new value.
+	 * Then we sort the combine ranges - this is necessary, because although
+	 * ranges and points were sorted on their own, the new array is not. We
+	 * do that by performing a merge sort of the two parts.
 	 *
 	 * The distanceFn calls (which may internally call e.g. numeric_le) may
-	 * allocate quite a bit of memory, and we must not leak it. Otherwise
-	 * we'd have problems e.g. when building indexes. So we create a local
-	 * memory context and make sure we free the memory before leaving this
-	 * function (not after every call).
+	 * allocate quite a bit of memory, and we must not leak it (we might have
+	 * to do this repeatedly, even for a single BRIN page range). Otherwise
+	 * we'd have problems e.g. when building new indexes. So we use a memory
+	 * context and make sure we free the memory at the end (so if we call
+	 * the distance function many times, it might be an issue, but meh).
 	 */
 	ctx = AllocSetContextCreate(CurrentMemoryContext,
 								"minmax-multi context",
@@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	oldctx = MemoryContextSwitchTo(ctx);
 
 	/* OK build the combine ranges */
-	cranges = build_combine_ranges(cmpFn, colloid, ranges,
-								   true, newval, &ncranges,
-								   false);
+	cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges);
 
 	/* and we'll also need the 'distance' procedure */
 	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
@@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	 * use too low or high value.
 	 */
 	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
-									 ranges->maxvalues * MINMAX_LOAD_FACTOR,
+									 range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR,
 									 cmpFn, colloid);
 
-	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR);
+	Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR);
 
 	/* decompose the combine ranges into regular ranges and single values */
-	store_combine_ranges(ranges, cranges, ncranges);
+	store_combine_ranges(range, cranges, ncranges);
 
 	MemoryContextSwitchTo(oldctx);
 	MemoryContextDelete(ctx);
 
 	/* Did we break the ranges somehow? */
+	AssertCheckRanges(range, cmpFn, colloid);
+
+	return true;
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn;
+	bool		modified = false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
 	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Make sure there's enough free space in the buffer. We only trigger
+	 * this when the buffer is full, which means it had to be modified as
+	 * we size it to be larger than what is stored on disk.
+	 *
+	 * XXX This needs to happen before we check if the value is contained
+	 * in the range, because the value might be in the unsorted part, and
+	 * we don't check that in range_contains_value. The deduplication would
+	 * then move it to the sorted part, and we'd add the value too, which
+	 * violates the rule that we never have duplicates with the ranges
+	 * or sorted values.
+	 *
+	 * XXX At the moment this only does the deduplication.
+	 *
+	 * XXX We might also deduplicate and recheck if the value is contained,
+	 * but that seems like an overkill. We'd need to deduplicate anyway,
+	 * so why not do it now.
+	 */
+	modified = ensure_free_space_in_buffer(bdesc, colloid,
+										   attno, attr, ranges);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return modified;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	ranges->values[2*ranges->nranges + ranges->nvalues] = newval;
+	ranges->nvalues++;
+
+	/*
+	 * Check we haven't broken the ordering of boundary values (checks
+	 * both parts, but that doesn't hurt).
+	 */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/* Also check the range contains the value we just added. */
 	// FIXME Assert(ranges, cmpFn, colloid);
 
+	/* yep, we've modified the range */
 	return true;
 }
 
@@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
 	MemoryContext	ctx;
 	MemoryContext	oldctx;
 
-	/*
-	 * This should only be used in batch mode, and there should be no
-	 * ranges, just individual values.
-	 */
-	Assert((ranges->batch_mode) && (ranges->nranges == 0));
-
 	/* we'll certainly need the comparator, so just look it up now */
 	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
 											   BTLessStrategyNumber);
@@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
 
 	/* OK build the combine ranges */
 	cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges,
-								   false, (Datum) 0, &ncranges,
-								   true);	/* deduplicate */
+								   &ncranges);	/* deduplicate */
 
 	if (ncranges > 1)
 	{
@@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
 		 * don't expect more tuples to be inserted soon.
 		 */
 		ncranges = reduce_combine_ranges(cranges, ncranges, distances,
-										  max_values, cmpFn, ranges->colloid);
+										 max_values, cmpFn, ranges->colloid);
 
 		Assert(count_values(cranges, ncranges) <= max_values);
 	}
@@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
 	 * In batch mode, we need to compress the accumulated values to the
 	 * actually requested number of values/ranges.
 	 */
-	if (ranges->batch_mode)
-		compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+	compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
 
 	s = range_serialize(ranges);
 	dst[0] = PointerGetDatum(s);
@@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	{
 		MemoryContext oldctx;
 
+		int				target_maxvalues;
+		int				maxvalues;
 		BlockNumber		pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
 
+		/* what was specified as a reloption? */
+		target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped
+		 * to the maximum number of values in the heap range. This is more
+		 * than enough, considering the actual number of rows per page is
+		 * likely much lower, but meh.
+		 */
+		maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, target_maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
 		oldctx = MemoryContextSwitchTo(column->bv_context);
-		ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange);
+		ranges = minmax_multi_init(maxvalues);
 		ranges->attno = attno;
 		ranges->colloid = colloid;
 		ranges->typid = attr->atttypid;
-		ranges->batch_mode = true;
-		ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+		ranges->target_maxvalues = target_maxvalues;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
 
 		MemoryContextSwitchTo(oldctx);
 
@@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	{
 		MemoryContext oldctx;
 
+		int				maxvalues;
+		BlockNumber		pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
 		oldctx = MemoryContextSwitchTo(column->bv_context);
 
 		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-		ranges = range_deserialize(serialized);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped
+		 * to the maximum number of values in the heap range. This is more
+		 * than enough, considering the actual number of rows per page is
+		 * likely much lower, but meh.
+		 */
+		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, serialized->maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		ranges = range_deserialize(maxvalues, serialized);
+
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
 
 		column->bv_mem_value = PointerGetDatum(ranges);
 		column->bv_serialize = brin_minmax_multi_serialize;
@@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
 	attno = column->bv_attno;
 
 	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-	ranges = range_deserialize(serialized);
+	ranges = range_deserialize(serialized->maxvalues, serialized);
 
 	/* inspect the ranges, and for each one evaluate the scan keys */
 	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
@@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
 	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
 
-	ranges_a = range_deserialize(serialized_a);
-	ranges_b = range_deserialize(serialized_b);
+	ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a);
+	ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b);
 
 	/* make sure neither of the ranges is NULL */
 	Assert(ranges_a && ranges_b);
@@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 											 BTLessStrategyNumber);
 
-	/* sort the combine ranges (don't deduplicate) */
+	/* sort the combine ranges (no need to deduplicate) */
 	sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false);
 
 	/*
@@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
 	fmgr_info(outfunc, &fmgrinfo);
 
 	/* deserialize the range info easy-to-process pieces */
-	ranges_deserialized = range_deserialize(ranges);
+	ranges_deserialized = range_deserialize(ranges->maxvalues, ranges);
 
 	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
 					 ranges_deserialized->nranges,
-- 
2.26.2

0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patchtext/x-patch; charset=UTF-8; name=0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patchDownload
From 8e67ef585c54b2ef5c7e3e2748375fac430165ff Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Tue, 2 Feb 2021 01:49:56 +0100
Subject: [PATCH 6/9] Batch mode when building new BRIN multi-minmax range

Strict enforcement of the values_per_range limit may be quite expensive,
particularly when adding many values into the same range, e.g. when
building a new index. This commit adds a "batch" mode which allows the
buffer to be much larger, so that the compaction happens only once at
the very end, as part of summary serialization. This amortizes the cost
as it's much more efficient to sort many values once.
---
 src/backend/access/brin/brin_minmax_multi.c | 227 ++++++++++++++++++--
 1 file changed, 204 insertions(+), 23 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 7c6d1c7447..d1eafa9700 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -57,6 +57,7 @@
 #include "access/brin.h"
 #include "access/brin_internal.h"
 #include "access/brin_tuple.h"
+#include "access/hash.h"	/* XXX strange that it fails because of BRIN_AM_OID without this */
 #include "access/reloptions.h"
 #include "access/stratnum.h"
 #include "access/htup_details.h"
@@ -150,13 +151,28 @@ typedef struct MinMaxOptions
  */
 typedef struct Ranges
 {
-	Oid		typid;
+	/* Cache information that we need quite often. */
+	Oid			typid;
+	Oid			colloid;
+	AttrNumber	attno;
 
 	/* (2*nranges + nvalues) <= maxvalues */
 	int		nranges;	/* number of ranges in the array (stored) */
 	int		nvalues;	/* number of values in the data array (all) */
 	int		maxvalues;	/* maximum number of values (reloption) */
 
+	/*
+	 * In batch mode, we simply add the values into a buffer, without any
+	 * expensive steps (sorting, deduplication, ...). The buffer is sized
+	 * to be larger than the target number of values per range, which
+	 * reduces the number of compactions - operating on larger buffers is
+	 * significantly more efficient, in most cases. We keep the actual
+	 * target and compact to the requested number of values at the very
+	 * end, before serializing to on-disk representation.
+	 */
+	bool	batch_mode;
+	int		target_maxvalues;	/* requested number of values */
+
 	/* values stored for this range - either raw values, or ranges */
 	Datum	values[FLEXIBLE_ARRAY_MEMBER];
 } Ranges;
@@ -376,6 +392,7 @@ range_deserialize(SerializedRanges *serialized)
 	range->nvalues = serialized->nvalues;
 	range->maxvalues = serialized->maxvalues;
 	range->typid = serialized->typid;
+	range->batch_mode = false;
 
 	typbyval = get_typbyval(serialized->typid);
 	typlen = get_typlen(serialized->typid);
@@ -753,10 +770,6 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
 	 */
 	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
 
-	/* then the single-point ranges (with nvalues boundary values ) */
-	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
-					 ranges->nvalues);
-
 	/* finally check that none of the values are not covered by ranges */
 	for (i = 0; i < ranges->nvalues; i++)
 	{
@@ -880,12 +893,22 @@ fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
 
 /*
  * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ *
+ * Optionally, the cranges may be deduplicated (this matters in batch mode,
+ * where we simply append values, without checking for duplicates etc.).
+ *
+ * Returns the number of combine ranges.
  */
-static void
+static int
 sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
-					CombineRange *cranges, int ncranges)
+					CombineRange *cranges, int ncranges,
+					bool deduplicate)
 {
-	compare_context cxt;
+	int				n;
+	int				i;
+	compare_context	cxt;
+
+	Assert(ncranges > 0);
 
 	/* sort the values */
 	cxt.colloid = colloid;
@@ -893,6 +916,26 @@ sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
 
 	qsort_arg(cranges, ncranges, sizeof(CombineRange),
 			  compare_combine_ranges, (void *) &cxt);
+
+	if (!deduplicate)
+		return ncranges;
+
+	/* optionally deduplicate the ranges */
+	n = 1;
+	for (i = 1; i < ncranges; i++)
+	{
+		if (compare_combine_ranges(&cranges[i-1], &cranges[i], (void *) &cxt))
+		{
+			if (i != n)
+				memcpy(&cranges[n], &cranges[i], sizeof(CombineRange));
+
+			n++;
+		}
+	}
+
+	Assert((n > 0) && (n <= ncranges));
+
+	return n;
 }
 
 /*
@@ -1063,26 +1106,40 @@ build_distances(FmgrInfo *distanceFn, Oid colloid,
  */
 static CombineRange *
 build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
-					 Datum newvalue, int *nranges)
+					 bool addvalue, Datum newvalue, int *nranges,
+					 bool deduplicate)
 {
 	int				ncranges;
 	CombineRange   *cranges;
 
 	/* now do the actual merge sort */
-	ncranges = ranges->nranges + ranges->nvalues + 1;
+	ncranges = ranges->nranges + ranges->nvalues;
+
+	/* should we add an extra value? */
+	if (addvalue)
+		ncranges += 1;
+
 	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
-	*nranges = ncranges;
 
 	/* put the new value at the beginning */
-	cranges[0].minval = newvalue;
-	cranges[0].maxval = newvalue;
-	cranges[0].collapsed = true;
+	if (addvalue)
+	{
+		cranges[0].minval = newvalue;
+		cranges[0].maxval = newvalue;
+		cranges[0].collapsed = true;
 
-	/* then the regular and collapsed ranges */
-	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+		/* then the regular and collapsed ranges */
+		fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+	}
+	else
+		fill_combine_ranges(cranges, ncranges, ranges);
 
 	/* and sort the ranges */
-	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+	ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges,
+								   deduplicate);
+
+	/* remember how many cranges we built */
+	*nranges = ncranges;
 
 	return cranges;
 }
@@ -1295,6 +1352,27 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	/* comprehensive checks of the input ranges */
 	AssertCheckRanges(ranges, cmpFn, colloid);
 
+	Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0));
+
+	/*
+	 * When batch-building, there should be no ranges. So either the
+	 * number of ranges is 0 or we're not in batching mode.
+	 */
+	Assert(!ranges->batch_mode || (ranges->nranges == 0));
+
+	/* When batch-building, just add it and we're done. */
+	if (ranges->batch_mode)
+	{
+		/* there has to be free space, if we've sized the struct */
+		Assert(ranges->nvalues < ranges->maxvalues);
+
+		/* Make a copy of the value, if needed. */
+		ranges->values[ranges->nvalues++]
+			= datumCopy(newval, attr->attbyval, attr->attlen);;
+
+		return true;
+	}
+
 	/*
 	 * Bail out if the value already is covered by the range.
 	 *
@@ -1375,7 +1453,9 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	oldctx = MemoryContextSwitchTo(ctx);
 
 	/* OK build the combine ranges */
-	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+	cranges = build_combine_ranges(cmpFn, colloid, ranges,
+								   true, newval, &ncranges,
+								   false);
 
 	/* and we'll also need the 'distance' procedure */
 	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
@@ -1407,6 +1487,82 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	return true;
 }
 
+/*
+ * Generate range representation of data collected during "batch mode".
+ * This is similar to reduce_combine_ranges, except that we can't assume
+ * the values are sorted and there may be duplicate values.
+ */
+static void
+compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	/*
+	 * This should only be used in batch mode, and there should be no
+	 * ranges, just individual values.
+	 */
+	Assert((ranges->batch_mode) && (ranges->nranges == 0));
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
+											   BTLessStrategyNumber);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, ranges->attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges,
+								   false, (Datum) 0, &ncranges,
+								   true);	/* deduplicate */
+
+	if (ncranges > 1)
+	{
+		/* build array of gap distances and sort them in ascending order */
+		distances = build_distances(distanceFn, ranges->colloid, cranges, ncranges);
+
+		/*
+		 * Combine ranges until we get below max_values. We don't use any scale
+		 * factor, because this is used at the very end of "batch mode" and we
+		 * don't expect more tuples to be inserted soon.
+		 */
+		ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+										  max_values, cmpFn, ranges->colloid);
+
+		Assert(count_values(cranges, ncranges) <= max_values);
+	}
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	/* check all the range invariants */
+	AssertCheckRanges(ranges, cmpFn, ranges->colloid);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+}
+
 Datum
 brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 {
@@ -1890,7 +2046,16 @@ static void
 brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
 {
 	Ranges *ranges = (Ranges *) DatumGetPointer(src);
-	SerializedRanges *s = range_serialize(ranges);
+	SerializedRanges *s;
+
+	/*
+	 * In batch mode, we need to compress the accumulated values to the
+	 * actually requested number of values/ranges.
+	 */
+	if (ranges->batch_mode)
+		compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+
+	s = range_serialize(ranges);
 	dst[0] = PointerGetDatum(s);
 }
 
@@ -1933,15 +2098,31 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	/*
 	 * If this is the first non-null value, we need to initialize the range
 	 * list. Otherwise just extract the existing range list from BrinValues.
+	 *
+	 * When starting with an empty range, we assume this is a batch mode,
+	 * i.e. we size the buffer for the maximum possible number of items in
+	 * the range (based on range size and max number of items on a page).
+	 *
+	 * XXX This may require quite a bit of memory, so maybe we should use
+	 * some value in between. OTOH most tables will have much wider rows,
+	 * so the number of rows per page is much lower.
+	 *
+	 * XXX Maybe we should do this (using larger buffer) even when there
+	 * already is a summary?
 	 */
 	if (column->bv_allnulls)
 	{
 		MemoryContext oldctx;
 
-		oldctx = MemoryContextSwitchTo(column->bv_context);
+		BlockNumber		pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
 
-		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+		ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange);
+		ranges->attno = attno;
+		ranges->colloid = colloid;
 		ranges->typid = attr->atttypid;
+		ranges->batch_mode = true;
+		ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
 
 		MemoryContextSwitchTo(oldctx);
 
@@ -2227,8 +2408,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 											 BTLessStrategyNumber);
 
-	/* sort the combine ranges */
-	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+	/* sort the combine ranges (don't deduplicate) */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false);
 
 	/*
 	 * We've merged two different lists of ranges, so some of them may be
-- 
2.26.2

0005-BRIN-minmax-multi-indexes-20210203.patchtext/x-patch; charset=UTF-8; name=0005-BRIN-minmax-multi-indexes-20210203.patchDownload
From 5a0c8fcfc16887c0efdc9dfd2691365e5c763d01 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Wed, 3 Feb 2021 19:00:00 +0100
Subject: [PATCH 5/9] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2579 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   18 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    8 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 ++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   85 +
 src/include/catalog/pg_type.dat             |    6 +
 src/test/regress/expected/brin_multi.out    |  445 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  397 +++
 21 files changed, 5041 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 940a0fd8fd..f8a82205d8 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -612,6 +612,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     8 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..7c6d1c7447
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2579 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+#define		MINMAX_LOAD_FACTOR		0.75
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0))
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, with a simple header
+ * with basic metadata, followed by the boundary values. It has a varlena
+ * header, so can be treated as varlena directly.
+ *
+ * See range_serialize/range_deserialize for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * compare_values
+ *	  Compare the values.
+ */
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum *da = (Datum *) a;
+	Datum *db = (Datum *) b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *da, *db);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *db, *da);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+}
+#endif
+
+/*
+ * Comprehensive check of the Ranges structure.
+ */
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i, j;
+
+	/* some basic sanity checks */
+	Assert(ranges->nranges >= 0);
+	Assert(ranges->nvalues >= 0);
+	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
+	Assert(ranges->typid != InvalidOid);
+
+	/*
+	 * First the ranges - there are 2*nranges boundary values, and the
+	 * values have to be strictly ordered (equal values would mean the
+	 * range is collapsed, and should be stored as a point). This also
+	 * guarantees that the ranges do not overlap.
+	 */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundary values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+
+	/* finally check that none of the values are not covered by ranges */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	value = ranges->values[2 * ranges->nranges + i];
+
+		for (j = 0; j < ranges->nranges; j++)
+		{
+			Datum	r;
+
+			Datum	minval = ranges->values[2 * j];
+			Datum	maxval = ranges->values[2 * j + 1];
+
+			/* if value is smaller than range minimum, that's OK */
+			r = FunctionCall2Coll(cmpFn, colloid, value, minval);
+			if (DatumGetBool(r))
+				continue;
+
+			/* if value is greater than range maximum, that's OK */
+			r = FunctionCall2Coll(cmpFn, colloid, maxval, value);
+			if (DatumGetBool(r))
+				continue;
+
+			/* value is between [min,max], which is wrong */
+			Assert(false);
+		}
+	}
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ * This is intentionally sorting the distances in descending order, i.e.
+ * the longer gaps will be at the front.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return 1;
+	else if (da->value > db->value)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (max-min) for pairs
+ * of consecutive ranges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the gap (between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index of the gap the distance is for */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/*
+	 * Sort the distances in descending order, so that the longest gaps
+	 * are at the front.
+	 */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue),
+			 compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+#endif
+
+/*
+ * reduce_combine_ranges
+ *		reduce the ranges until the number of values is low enough
+ *
+ * Combines ranges until the number of boundary values drops below the
+ * threshold specified by max_values. This happens by merging enough
+ * ranges by distance between them.
+ *
+ * Returns the number of result ranges.
+ *
+ * We simply use the global min/max and then add boundaries for enough
+ * largest gaps. Each gap adds 2 values, so we simply use (target/2-1)
+ * distances. Then we simply sort all the values - each two values are
+ * a boundary of a range (possibly collapsed).
+ *
+ * XXX Some of the ranges may be collapsed (i.e. the min/max values are
+ * equal), but we ignore that for now. We could repeat the process,
+ * adding a couple more gaps recursively.
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ *
+ * XXX Or maybe we could just handle this by using random value as a
+ * tie-break, or by adding random noise to the actual distance.
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values,
+					  FmgrInfo *cmp, Oid colloid)
+{
+	int i;
+	int		nvalues;
+	Datum  *values;
+
+	compare_context cxt;
+
+	/* total number of gaps between ranges */
+	int	ndistances = (ncranges - 1);
+
+	/* number of gaps to keep */
+	int keep = (max_values/2 - 1);
+
+	/*
+	 * Maybe we have sufficiently low number of ranges already?
+	 *
+	 * XXX This should happen before we actually do the expensive stuff
+	 * like sorting, so maybe this should be just an assert.
+	 */
+	if (keep >= ndistances)
+		return ncranges;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	/* add the global min/max values, from the first/last range */
+	values[nvalues++] = cranges[0].minval;
+	values[nvalues++] = cranges[ncranges-1].maxval;
+
+	/* add boundary values for enough gaps */
+	for (i = 0; i < keep; i++)
+	{
+		/* index of the gap between (index) and (index+1) ranges */
+		int index = distances[i].index;
+
+		Assert((index >= 0) && ((index+1) < ncranges));
+
+		/* add max from the preceding range, minval from the next one */
+		values[nvalues++] = cranges[index].maxval;
+		values[nvalues++] = cranges[index+1].minval;
+
+		Assert(nvalues <= max_values);
+	}
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/*
+	 * Sort the values using the comparator function, and form ranges
+	 * from the sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		cranges[i].minval = values[2*i];
+		cranges[i].maxval = values[2*i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		cranges[i].collapsed = (compare_values(&values[2*i],
+											   &values[2*i+1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+
+	Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues);
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertCheckRanges(ranges, cmpFn, colloid);
+
+		/* Also check the range contains the value we just added. */
+		// FIXME Assert(ranges, cmpFn, colloid);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 *
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues * MINMAX_LOAD_FACTOR,
+									 cmpFn, colloid);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Did we break the ranges somehow? */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+	// FIXME Assert(ranges, cmpFn, colloid);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	/*
+	 * We use the no-check variants here, because user-supplied values
+	 * may have (ip_posid == 0). See ItemPointerCompare.
+	 */
+	da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumberNoCheck(pa1);
+
+	da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumberNoCheck(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute approximate distance between two dates.
+ */
+Datum
+brin_minmax_multi_distance_date(PG_FUNCTION_ARGS)
+{
+	DateADT		dateVal1 = PG_GETARG_DATEADT(0);
+	DateADT		dateVal2 = PG_GETARG_DATEADT(1);
+
+	PG_RETURN_FLOAT8(dateVal1 - dateVal2);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+Datum
+brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	Timestamp	dt1 = PG_GETARG_TIMESTAMP(0);
+	Timestamp	dt2 = PG_GETARG_TIMESTAMP(1);
+
+	if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
+		PG_RETURN_FLOAT8(0);
+
+	delta = dt2 - dt1;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	Interval   *ia = PG_GETARG_INTERVAL_P(0);
+	Interval   *ib = PG_GETARG_INTERVAL_P(1);
+	Interval   *result;
+
+	result = (Interval *) palloc(sizeof(Interval));
+
+	result->month = ib->month - ia->month;
+	/* overflow check copied from int4mi */
+	if (!SAMESIGN(ib->month, ia->month) &&
+		!SAMESIGN(result->month, ib->month))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->day = ib->day - ia->day;
+	if (!SAMESIGN(ib->day, ia->day) &&
+		!SAMESIGN(result->day, ib->day))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->time = ib->time - ia->time;
+	if (!SAMESIGN(ib->time, ia->time) &&
+		!SAMESIGN(result->time, ib->time))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	/*
+	 * We assume months have 31 days - we don't need to be precise, in
+	 * the worst case we'll build somewhat less efficient ranges.
+	 */
+	delta = (double) (result->month * 31 + result->day);
+
+	/* convert to microseconds (just like the time part) */
+	delta = 24L * 3600L * 1000000L * delta;
+
+	/* and add the time part */
+	delta += result->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/*
+	 * If needed, reduce some of the ranges.
+	 *
+	 * XXX This may be fairly expensive, so maybe we should do it only when
+	 * it's actually needed (when we have too many ranges).
+	 */
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges,
+	 * and if needed combine as many off them to get below the threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX This does not apply the load factor, as we don't expect to
+	 * add more values to the range, so we prefer to keep as many ranges
+	 * as possible.
+	 *
+	 * XXX Can the maxvalues be different in the two ranges? Perhaps
+	 * we should use maximum of those?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues,
+									 cmpFn, colloid);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 8, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..bf8635d788 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,15 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				brdesc,
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -495,6 +504,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +588,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 8cc4e532e6..fdaff42722 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..87de94f397 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,11 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+/*
+ * The BRIN opclasses may register serialization callback, in case the on-disk
+ * and in-memory representations differ (e.g. for performance reasons).
+ */
+typedef void (*brin_serialize_callback_type) (BrinDesc *bdesc, Datum src, Datum *dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +32,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index e07dd83550..e2a04d9cb0 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 616329df8f..762ac3acef 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2086,6 +2232,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2112,6 +2275,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2179,6 +2358,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2210,6 +2455,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2232,6 +2494,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2254,6 +2533,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2318,6 +2614,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2469,6 +2782,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2524,6 +2983,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2546,6 +3022,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2602,6 +3095,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2624,6 +3134,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2690,6 +3217,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 6709c8dfea..51403716b1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index dea9adaf98..ba9231ac8c 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9926',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '9901',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9927',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '9902',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9928',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '9904',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9929',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '9905',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9930',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '9909',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '9910',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9931',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9932',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '9911',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9933',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '9912',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9934',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '9913',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9935',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '9914',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9936',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '9916',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9937',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '9917',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9938',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '9918',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9939',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '9919',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b7432c65d5..dbf0ee13b2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8197,6 +8197,77 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9940', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9941', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9942', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9943', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9944', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9945', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9946', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9947', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9948', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9949', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9950', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9951', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9952', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9953', descr => 'BRIN multi minmax date distance',
+  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
+{ oid => '9954', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9955', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9956', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9957', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9958', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9959', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9960', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+{ oid => '9961', descr => 'BRIN multi minmax timestamp distance',
+  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11421,4 +11492,18 @@
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
 
+{ oid => '9962', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9963', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9964', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9965', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 74e279cbf9..e809094490 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -685,4 +685,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+{ oid => '9966',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..e13cb59c7e
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,445 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+ERROR:  value 7 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+DROP INDEX brinidx_multi;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index e568b9fea2..0541c12a25 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9925 | pg_brin_bloom_summary
-(5 rows)
+ 9966 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f1fed1037d..bfbbdbc894 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 9ed1468ad8..54d24cc184 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..6d61fb84c6
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,397 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+
+DROP INDEX brinidx_multi;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0004-BRIN-bloom-indexes-20210203.patchtext/x-patch; charset=UTF-8; name=0004-BRIN-bloom-indexes-20210203.patchDownload
From 13cc46c06f5aa02a7821fa59a3dbc65630d83be5 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 4/9] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 178 +++++
 doc/src/sgml/ref/create_index.sgml        |  31 +
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 784 ++++++++++++++++++++++
 src/include/access/brin.h                 |   2 +
 src/include/access/brin_internal.h        |   4 +
 src/include/catalog/pg_amop.dat           | 170 +++++
 src/include/catalog/pg_amproc.dat         | 447 ++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 ++
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +-
 src/test/regress/expected/brin_bloom.out  | 456 +++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 404 +++++++++++
 19 files changed, 2641 insertions(+), 6 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index a5271a9f8f..940a0fd8fd 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -581,6 +581,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default value is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default value is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..15a8258677
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,784 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test whether it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_page.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001		/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25		/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+
+#define BloomMaxFilterSize \
+	MAXALIGN_DOWN(BLCKSZ - \
+				  (MAXALIGN(SizeOfPageHeaderData + \
+							sizeof(ItemIdData)) + \
+				   MAXALIGN(sizeof(BrinSpecialSpace)) + \
+				   SizeOfBrinTuple))
+
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * To calculate the additional hashes (treating the uint32 hash as an input
+ * value), we use the approach with two hash functions from this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions are calculated using hard-coded seeds.
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes
+ * that are not entirely 0. But while indexes don't support TOAST, the
+ * varlena can still be compressed. So this seems unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high. But even if the false positive rate
+ * exceeds the desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16	flags;
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		nbits;	/* size of filter / number of bits */
+	int		nbytes;	/* size of filter / number of bytes */
+
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	nbits = ceil(- (ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	nbytes = ((nbits + 7) / 8);
+	nbits = nbytes * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * Initially the bloom filter is just zeroes and so very compressible,
+	 * but as we add values it gets more and more random, and so less and
+	 * less compressible. So initially everything fits on the page, but
+	 * we might get surprising failures later - we want to prevent that,
+	 * so we reject bloom filter that are obviously too large.
+	 *
+	 * XXX It's not uncommon to oversize the bloom filter a bit, to defend
+	 * against unexpected data anomalies (parts of table with more distinct
+	 * values per range etc.). But we still need to make sure even the
+	 * oversized filter fits on page, if such need arises.
+	 *
+	 * XXX This check is not perfect, because the index may have multiple
+	 * filters that are small individually, but too large when combined.
+	 */
+	if (nbytes > BloomMaxFilterSize)
+		elog(ERROR, "the bloom filter is too large (%d > %zu)", nbytes,
+			 BloomMaxFilterSize);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * nbits / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so
+	 * the varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + nbytes;
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = nbits;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	h1, h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	h1, h2;
+
+	/* calculate the two hashes */
+	h1 = hash_uint32_extended(value, 0x71d924af) % filter->nbits;
+	h2 = hash_uint32_extended(value, 0xba48b314) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters for now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int		i;
+	int		nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 79440ebe7b..8cc4e532e6 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..616329df8f 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,44 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2064,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2086,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2108,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2179,20 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2210,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2232,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2254,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2296,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2318,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2469,44 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2524,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2546,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2602,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2624,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2690,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..6709c8dfea 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..dea9adaf98 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9901',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '9902',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '9903',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9904',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9905',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '9906',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '9907',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '9908',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9909',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9910',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9911',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9912',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9913',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '9914',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '9915',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9916',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9917',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9918',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9919',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4099e72001..b7432c65d5 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8214,6 +8214,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '9920', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '9921', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '9922', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '9923', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '9924', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11387,4 +11407,18 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8959c2f53b..74e279cbf9 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,5 +679,10 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-
+{ oid => '9925',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..24ea5f6e42
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,456 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..e568b9fea2 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9925 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 12bb67e491..f1fed1037d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 59b416fd80..9ed1468ad8 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..d587f3962f
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,404 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0003-Optimize-allocations-in-bringetbitmap-20210203.patchtext/x-patch; charset=UTF-8; name=0003-Optimize-allocations-in-bringetbitmap-20210203.patchDownload
From d2f1607d5c1c132135f1cf95a4a26ec7c5401754 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/9] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 14da9ed17f..3735c41788 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -398,11 +401,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -438,9 +480,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -451,17 +493,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210203.patchtext/x-patch; charset=UTF-8; name=0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210203.patchDownload
From 9ec845f064298be591f259f5b7ed2098c843dd3d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/9] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 239 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index dc187153aa..14da9ed17f 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -179,7 +182,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -243,31 +245,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -390,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -416,10 +396,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -440,23 +423,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -464,9 +448,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -544,15 +542,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -695,7 +735,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -724,25 +763,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1521,6 +1543,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1574,3 +1629,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 215bc794d3..f4730be3b9 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -572,37 +504,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 12878ff3a0..6c8852d404 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 78c89a6961..79440ebe7b 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210203.patchtext/x-patch; charset=UTF-8; name=0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210203.patchDownload
From 93989c531da939a71023561463db54760c6ebb3f Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/9] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 27ba596c6e..dc187153aa 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -390,6 +390,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -411,6 +414,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -471,7 +529,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -481,51 +539,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..215bc794d3 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +442,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +462,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +481,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +516,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +526,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..12878ff3a0 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..11835d85cd 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4e0c9be58c..4099e72001 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8191,7 +8191,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8207,7 +8207,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

brin-bench.odsapplication/vnd.oasis.opendocument.spreadsheet; name=brin-bench.odsDownload
brin-stress-test.tgzapplication/x-compressed-tar; name=brin-stress-test.tgzDownload
brin-bench.tgzapplication/x-compressed-tar; name=brin-bench.tgzDownload
����s���}k�[���%,0������7���4�EO�����H�v���J�6?,i���]�nfb'BzX��������v���������K���?���YECg��4��������}S��93L]�]����q�}��=#zU;�7Vad���^�f�W�\���Wc��[�\�~����|�0��}��w�����O�%��Z���~��i�x�=&��3��''�B�0�&�"��$�������W�-������Z,H��?��o������d�r'���$�\�[�����q��E�7$��$VH>7>7�1�9�F�H�K�{�Qg���a���I��[����?�,,w��f���v�V��B�@���E��L��&��;k'��y�	�X���]x��Id��D�i%��.��ug�� ��c��N��wZdxK2�.
�3���
�F���2-�u
�����VKb���?���+|�dN'_|����j��}�z�����A���g�����(�+~!G��m^F���{M�z	�����J�����-�=M;��^��j�N�qP�V���v�9u������_O����d/���I@���	���d~�.X�����=;y}�wgI_7�;�S�&�����z�B.����it�w�v��N�����	B^�!�{��6���3���1��F@Qr����K���r�e����G'����&v8�i0b��������G,��arSY-"G�d�F��G���<?l����.�t�Z2����,���c�"`&�)JH*=�"D������dO���o��:�M|���]�m���
g����5��0���A:���H2��'��B+���.6�3��G�6���!�$A�oT���EU�+��"0��&A)���
��$&�2*;�2t��N9.{�Mgi=g��M��v���e����H��3�����J�:V��5�l�j1bAQFi� 	��tD��{�C;����L����T�o�e(����K\R�E��C�s�#�x�u8�@CB��FPS�$]
U %��:xJS��#&�A��� '���������v\�[BuH!��>����)L2��H��	I.!d .:P�`�0�A�����������i�?��V�%s��'���)>�2`�N����x0	
G���4E��ms���GN=��2�U��\J�R.���p�q�sRN�j���ce	���{r
O.FS��/q���=�������_�#��2���_�M������2���5vf�Z���44����pl
�T��t8����6�����tc�F:�'���I�O��9���+��+fNR���0���o�a�ODA�V�C�0��4��$8b�>x�z�0/*�	��*i��.��0��KIbj|&}�������}!v�d/	!
'���>a��n4�$T��	�)M�n.���x��8(�p@�e� �_����,r��	{'�A��`
	%@M�
�t5 T��p�@|T�)ALa�R��'}���r��x�@@b��C�`�����i7����C\X`QP��r�$���}��������@I���LqS�b�>a�W��$N7:�
v�%d��	}�����{���O.XEQ��-���L	G=7!�������	��B�0��w�$�\*0� �M)^���3�y���+2+l�����_����K�/�g��F��E���Q���{�Df���C/7\?�x���D!:���o���J12�R�)��!�(���!��+2���� �aP��~w!�|!�'�����&�F���D�X�1b��P)I��R].�7J�2�q"	�!(��K��f��:RFy�:w����?�=	V�p�;U��s�@������*ET��L.HES��/���H�JpcL2�������&��IJ*s�SoN�Xd�J>hFT��0+0r�`P�Q�I�������	����T��Td�V
rNFUx�$1�:qH�VpM:��>�CF�.�f%�Ej���R��d�a��@9���<e��Q�)��t����i��d����p����TF�) TC#�����P|QN��W�p@U��5�@)�H��$H%
I�d�TS!GX��Q-����'H(��%�Q=N����DJl����O����s������s�QC��A����3;<V����F{_)?8����PHw94P�r(�DB��/�� ��hp@�Nu�.@�(�S�9D�<anV5u~�;H�#�i����[,��JIB
��������Q�H�@a~Xj|������
�����o�Gd���T
��U;��b:�o��\*�� ��K)^2�u�&���dRAI%�DR9M(���T��7��/��nAb�����
���b5F�z*2*h	5�< PQ�8!!^T2���6����VA�)���
��$&W_�2��I'�jr�@*xB'��$I�5Tm���LXGV��,25�7E=����&H~7m��&�=0Oe!�Bu�	�-��S���8������&��J�\"A2A6Q�;H'{$`�
9B4���iTgJANQ�H*Y@`��qB"���Y����^��T��'���2�|��s������cl������?W����1�3|>�I�F�[��X��hi����;��G/�B�0V�3%�"��v�Rr�_���hN��)z�xr"=o�ll�<o������!m�l\%$������/��~��O�&1����d�C�d@����?�t�2�i���>i<l/l-�ZF���?d?6]���@��������9��������������?qI��
#140Zhihong Yu
zyu@yugabyte.com
In reply to: Tomas Vondra (#139)
Re: WIP: BRIN multi-range indexes

Hi,
For 0007-Remove-the-special-batch-mode-use-a-larger--20210203.patch :

+       /* same as preceding value, so store it */
+       if (compare_values(&range->values[start + i - 1],
+                          &range->values[start + i],
+                          (void *) &cxt) == 0)
+           continue;
+
+       range->values[start + n] = range->values[start + i];

It seems the comment doesn't match the code: the value is stored when
subsequent value is different from the previous.

For has_matching_range():
+ int midpoint = (start + end) / 2;

I think the standard notion for midpoint is start + (end-start)/2.

+       /* this means we ran out of ranges in the last step */
+       if (start > end)
+           return false;

It seems the above should be ahead of computation of midpoint.

Similar comment for the code in AssertCheckRanges().

Cheers

On Wed, Feb 3, 2021 at 3:55 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

Show quoted text

Hi,

Here's an updated and significantly improved version of the patch
series, particularly the multi-minmax part. I've fixed a number of
stupid bugs in that, discovered by either valgrind or stress-tests.

I was surprised by some of the bugs, or rather that the existing
regression tests failed to crash on them, so it's probably worth briefly
discussing the details. There were two main classes of such bugs:

1) missing datumCopy

AFAICS this happened because there were a couple missing datumCopy
calls, and BRIN covers multiple pages, so with by-ref data types we
added a pointer but the buffer might have gone away unexpectedly.
Regular regression tests passed just fine, because brin_multi runs
almost separately, so the chance of the buffer being evicted was low.
Valgrind reported this (with a rather enigmatic message, as usual), and
so did a simple stress-test creating many indexes concurrently. Anything
causing aggressive eviction of buffer would do the trick, I think,
triggering segfaults, asserts, etc.

2) bogus opclass definitions

There were a couple opclasses referencing incorrect distance function,
intended for a different data type. I was scratching my head WTH the
regression tests pass, as there is a table to build multi-minmax index
on all supported data types. The reason is pretty silly - the table is
very small, just 100 rows, with very low fillfactor (so only couple
values per page), and the index was created with pages_per_range=1. So
the compaction was not triggered and the distance function was never
actually called. I've decided to build the indexes on a larger data set
first, to test this. But maybe this needs somewhat different approach.

BLOOM
-----

The attached patch series addresses comments from the last review. As
for the size limit, I've defined a new macro

#define BloomMaxFilterSize \
MAXALIGN_DOWN(BLCKSZ - \
(MAXALIGN(SizeOfPageHeaderData + \
sizeof(ItemIdData)) + \
MAXALIGN(sizeof(BrinSpecialSpace)) + \
SizeOfBrinTuple))

and use that to determine if the bloom filter is too large. IMO that's
close enough, considering that this is a best-effort check anyway (due
to not being able to consider multi-column indexes).

MINMAX-MULTI
------------

As mentioned, there's a lot of fixes and improvements in this part, but
the basic principle is still the same. I've kept it split into three
parts with different approaches to building, so that it's possible to do
benchmarks and comparisons, and pick the best one.

a) 0005 - Aggressively compacts the summary, by always keeping it within
the limit defined by values_per_range. So if the range contains more
values, this may trigger compaction very often in some cases (e.g. for
monotonic series).

One drawback is that the more often the compactions happen, the less
optimal the result is - the algorithm is kinda greedy, picking something
like local optimums in each step.

b) 0006 - Batch build, exactly the opposite of 0005. Accumulates all
values in a buffer, then does a single round of compaction at the very
end. This obviously doesn't have the "greediness" issues, but it may
consume quite a bit of memory for some data types and/or indexes with
large BRIN ranges.

c) 0007 - A hybrid approach, using a buffer that is multiple of the
user-specified value, with some safety min/max limits. IMO this is what
we should use, although perhaps with some tuning of the exact limits.

Attached is a spreadsheet with benchmark results for each of those three
approaches, on different data types (byval/byref), data set types, index
parameters (pages/values per range) etc. I think 0007 is a reasonable
compromise overall, with performance somewhere in betwen 0005 and 0006.
Of course, there are cases where it's somewhat slow, e.g. for data types
with expensive comparisons and data sets forcing frequent compactions,
in which case it's ~10x slower compared to regular minmax (in most cases
it's ~1.5x). Compared to btree, it's usually much faster - ~2-3x as fast
(except for some extreme cases, of course).

As for the opclasses for indexes without "natural" distance function,
implemented in 0008, I think we should drop it. In theory it works, but
I'm far from convinced it's actually useful in practice. Essentially, if
you have a data type with ordering but without a meaningful concept of a
distance, it's hard to say what is an outlier. I'd bet most of those
data types are used as "labels" where even the ordering is kinda
useless, i.e. hardly anyone uses range queries on things like names,
it's all just equality searches. Which means the bloom indexes are a
much better match for this use case.

The other thing we were considering was using the new multi-minmax
opclasses as default ones, replacing the existing minmax ones. IMHO we
shouldn't do that either. For existing minmax indexes that's useless
(the opclass seems to be working, otherwise the index would be dropped).
But even for new indexes I'm not sure it's the right thing, so I don't
plan to change this.

I'm also attaching the stress-test that I used to test the hell out of
this, creating indexes on various data sets, data types, with varying
index parameters, etc.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#141Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Zhihong Yu (#140)
Re: WIP: BRIN multi-range indexes

On 2/4/21 1:49 AM, Zhihong Yu wrote:

Hi,
For 0007-Remove-the-special-batch-mode-use-a-larger--20210203.patch :

+       /* same as preceding value, so store it */
+       if (compare_values(&range->values[start + i - 1],
+                          &range->values[start + i],
+                          (void *) &cxt) == 0)
+           continue;
+
+       range->values[start + n] = range->values[start + i];

It seems the comment doesn't match the code: the value is stored when
subsequent value is different from the previous.

Yeah, you're right the comment is wrong - the code is doing exactly the
opposite. I'll need to go through this more carefully.

For has_matching_range():
+       int     midpoint = (start + end) / 2;

I think the standard notion for midpoint is start + (end-start)/2.

+       /* this means we ran out of ranges in the last step */
+       if (start > end)
+           return false;

It seems the above should be ahead of computation of midpoint.

Not sure why would that be an issue, as we're not using the value and
the values are just plain integers (so no overflows ...).

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#142Zhihong Yu
zyu@yugabyte.com
In reply to: Tomas Vondra (#141)
Re: WIP: BRIN multi-range indexes

Hi,
bq. Not sure why would that be an issue

Moving the (start > end) check is up to your discretion.

But the midpoint computation should follow text book :-)

Cheers

On Wed, Feb 3, 2021 at 4:59 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

Show quoted text

On 2/4/21 1:49 AM, Zhihong Yu wrote:

Hi,
For 0007-Remove-the-special-batch-mode-use-a-larger--20210203.patch :

+       /* same as preceding value, so store it */
+       if (compare_values(&range->values[start + i - 1],
+                          &range->values[start + i],
+                          (void *) &cxt) == 0)
+           continue;
+
+       range->values[start + n] = range->values[start + i];

It seems the comment doesn't match the code: the value is stored when
subsequent value is different from the previous.

Yeah, you're right the comment is wrong - the code is doing exactly the
opposite. I'll need to go through this more carefully.

For has_matching_range():
+ int midpoint = (start + end) / 2;

I think the standard notion for midpoint is start + (end-start)/2.

+       /* this means we ran out of ranges in the last step */
+       if (start > end)
+           return false;

It seems the above should be ahead of computation of midpoint.

Not sure why would that be an issue, as we're not using the value and
the values are just plain integers (so no overflows ...).

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#143John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#139)
Re: WIP: BRIN multi-range indexes

On Wed, Feb 3, 2021 at 7:54 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

[v-20210203]

Hi Tomas,

I have some random comments from reading the patch, but haven't gone into
detail in the newer aspects. I'll do so in the near future.

The cfbot seems to crash on this patch during make check, but it doesn't
crash for me. I'm not even sure what date that cfbot status is from.

BLOOM
-----

Looks good, but make sure you change the commit message -- it still refers
to sorted mode.

+ * not entirely clear how to distrubute the space between those columns.

s/distrubute/distribute/

MINMAX-MULTI
------------

c) 0007 - A hybrid approach, using a buffer that is multiple of the
user-specified value, with some safety min/max limits. IMO this is what
we should use, although perhaps with some tuning of the exact limits.

That seems like a good approach.

+#include "access/hash.h" /* XXX strange that it fails because of
BRIN_AM_OID without this */

I think you want #include "catalog/pg_am.h" here.

Attached is a spreadsheet with benchmark results for each of those three
approaches, on different data types (byval/byref), data set types, index
parameters (pages/values per range) etc. I think 0007 is a reasonable
compromise overall, with performance somewhere in betwen 0005 and 0006.
Of course, there are cases where it's somewhat slow, e.g. for data types
with expensive comparisons and data sets forcing frequent compactions,
in which case it's ~10x slower compared to regular minmax (in most cases
it's ~1.5x). Compared to btree, it's usually much faster - ~2-3x as fast
(except for some extreme cases, of course).

As for the opclasses for indexes without "natural" distance function,
implemented in 0008, I think we should drop it. In theory it works, but

Sounds reasonable.

The other thing we were considering was using the new multi-minmax
opclasses as default ones, replacing the existing minmax ones. IMHO we
shouldn't do that either. For existing minmax indexes that's useless
(the opclass seems to be working, otherwise the index would be dropped).
But even for new indexes I'm not sure it's the right thing, so I don't
plan to change this.

Okay.

--
John Naylor
EDB: http://www.enterprisedb.com

#144Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#143)
Re: WIP: BRIN multi-range indexes

On 2/9/21 3:46 PM, John Naylor wrote:

On Wed, Feb 3, 2021 at 7:54 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

[v-20210203]

Hi Tomas,

I have some random comments from reading the patch, but haven't gone
into detail in the newer aspects. I'll do so in the near future.

The cfbot seems to crash on this patch during make check, but it doesn't
crash for me. I'm not even sure what date that cfbot status is from.

Yeah, I noticed that too, and I'm investigating.

I tried running the regression tests on a 32-bit machine (rpi4), which
sometimes uncovers strange failures, and that indeed fails. There are
two or three bugs.

Firstly, the allocation optimization patch does this:

MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys * natts)

instead of

MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * natts

and that sometimes produces the wrong result, triggering an assert.

Secondly, there seems to be an issue with cross-type bloom indexes.
Imagine you have an int8 column, with a bloom index on it, and then you
do this:

WHERE column = '122'::int4;

Currently, we end up passing this to the consistent function, which
tries to call hashint8 on the int4 datum - that succeeds on 64 bits
(because both types are byval), but fails on 32-bits (where int8 is
byref, so it fails on int4). Which causes a segfault.

I think I included those cross-type operators as a copy-paste from
minmax indexes, but I see hash indexes don't allow this. And removing
those cross-type rows from pg_amop.dat makes the crashes go away.

It's also possible I simplified the get_strategy_procinfo a bit too
much. I see the minmax variant has subtype, so maybe we could do this
instead (I recall the integer types should have "compatible" results).

There are a couple failues where the index does not produce the right
number of results, though. I haven't investigated that yet. Once I fix
this, I'll post an updated patch - hopefully that'll make cfbot happy.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#145Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#144)
9 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

Attached is an updated version of the patch series, addressing all the
failures on cfbot (at least I hope so). This turned out to be more fun
than I expected, as the issues went unnoticed on 64-bits and only failed
on 32-bits. That's also why I'm not entirely sure this will make cfbot
happy as that seems to be x86_64, but the issues are real so let's see.

1) I already outlined the issue in the previous message:

MAXALIGN(a * b) != MAXALIGN(a) * b

and there's an assert that we used exactly the same amount of memory we
allocated, so this caused a crash. Strange that it'd fail on 32-bits and
not 64-bits, but perhaps there's some math reason for that, or maybe it
was just pure luck.

2) The rest of the issues generally boils down to types that are byval
on 64-bits, but byref on 32-bits. Like int8 or float8 for example. The
first place causing issues were cross-type operators, i.e. the bloom
opclasses did things like this in pg_amop.dat:

{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
amoprighttype => 'int8', amopstrategy => '1',
amopopr => '=(int2,int8)', amopmethod => 'brin' },

so it was possible to do this:

WHERE int8column = 1234::int2

in which case we used the int8 opclass, so the consistent function
thought it's working with int8, and used the hash function defined for
that opclass in pg_amproc. That's hashint8 of course, but we called that
on Datum storing int2. Clearly, dereferencing that pointer is guaranteed
to fail with a segfault.

I think there are two options to fix this. Firstly, we can remove the
cross-type operators, so that the left/right type is always the same.
That'll work fine for most cases, and it's pretty simple. It's also what
the hash_ops opclasses do, so I've done that.

An alternative would be to do something like minmax does for stategies,
and consider the subtype (i.e. type of the right argument). It's trick a
bit tricky, though, because it assumes the hash functions for the two
types are "compatible" and produce the same hash for the same value.
AFAIK that's correct for the usual cases (int2/int4/int8) and it'd be
restricted by pg_amop. But hash_ops don't do that for some reason, so I
wonder what am I missing. (The other thing is where to define these hash
functions - right now pg_amproc only tracks hash function for the "base"
data type, and there may be multiple supported subtypes, so where to
store that? Perhaps we could use the hash function from the default hash
opclass for each type.)

Anyway, I've decided to keep this simple for now, and I've ripped-out
the cross-type operators. We can add that back later, if needed.

3) There were a couple byref failures in the distance functions, which
generally used "double" internally (which I'm not sure is guaranteed to
be 64-bit types) instead of float8, and used plain "return" instead of
PG_RETURN_FLOAT8() in a couple places. Silly mistakes.

4) A particulary funny mistake was in actually calculating the hashes
for bloom filter, which is using hash_uint32_extended (so that we can
seed it). The trouble is that while hash_uint32() returns uint32,
hash_uint32_extended returns ... uint64. So we calculated a hash, but
then used the *pointer* to the uint64 value, not the value. I have to
say, the "uint32" in the function name is somewhat misleading.

This passes all my tests, including valgrind on the 32-bit rpi4 machine,
the stress test (testing both the bloom and multi-minmax opclasses etc.)

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0009-Ignore-correlation-for-new-BRIN-opclasses-20210211.patchtext/x-patch; charset=UTF-8; name=0009-Ignore-correlation-for-new-BRIN-opclasses-20210211.patchDownload
From 84145a60750a3bff4454817a43e11fce610771e4 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 9/9] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index b54b963f87..af87737ae0 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -410,6 +410,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index d11ec73c81..4152b38c9e 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1798,6 +1798,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 47ca4ddbb5..bf40f9ce32 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index fdaff42722..5fbf8cf9c7 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0008-Define-multi-minmax-oclasses-for-types-with-20210211.patchtext/x-patch; charset=UTF-8; name=0008-Define-multi-minmax-oclasses-for-types-with-20210211.patchDownload
From d534e79cb734bb042c43152af5523031123c2f0d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Wed, 3 Feb 2021 19:12:05 +0100
Subject: [PATCH 8/9] Define multi-minmax oclasses for types without distance

The existing multi-minmax opclasses rely on "distance" function when
deciding how to build the ranges, covering all the values. In principle,
those opclasses try to minimize the lengths of ranges, i.e. maximize the
lengths of "gaps" between them.

For some data types it's hard to construct a distance function - types
like "text" or "name" generally serve as labels, and may have ordering
only. So this uses a different approach, based on the observation that
it's the outliers that "break" BRIN minmax indexes, i.e. the lowest and
highest values. So simply sort the values, keep the (K-2) extreme values
on either tail, and build a single range representing the values in the
middle. Of course, the question is whether this is actually useful, but
that's hard to judge, and it's the best we can do.
---
 src/backend/access/brin/brin_minmax_multi.c |  75 ++++++++++++
 src/include/catalog/pg_amop.dat             | 119 ++++++++++++++++++++
 src/include/catalog/pg_amproc.dat           | 113 +++++++++++++++++++
 src/include/catalog/pg_opclass.dat          |  21 ++++
 src/include/catalog/pg_opfamily.dat         |  14 +++
 5 files changed, 342 insertions(+)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 08d0d55b06..d11ec73c81 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1210,6 +1210,9 @@ build_distances(FmgrInfo *distanceFn, Oid colloid,
 
 	Assert(ncranges >= 2);
 
+	if (!distanceFn)
+		return NULL;
+
 	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
 
 	/*
@@ -1298,6 +1301,70 @@ count_values(CombineRange *cranges, int ncranges)
 }
 #endif
 
+
+static int
+reduce_combine_ranges_simple(CombineRange *cranges, int ncranges,
+							 int max_values, FmgrInfo *cmp, Oid colloid)
+{
+	int		i;
+	int		nvalues;
+	Datum  *values;
+
+	compare_context cxt;
+
+	/* number of values to keep on each tail */
+	int tail = (max_values - 2) / 2;
+	int m = Min(tail / 2, ncranges - 1 / 2);
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	for (i = 0; i < m; i++)
+	{
+		/* head */
+		values[nvalues++] = cranges[i].minval;
+		values[nvalues++] = cranges[i].maxval;
+
+		/* tail */
+		values[nvalues++] = cranges[ncranges - 1 - i].minval;
+		values[nvalues++] = cranges[ncranges - 1 - i].maxval;
+	}
+
+	/* middle part */
+	values[nvalues++] = cranges[m].maxval;
+	values[nvalues++] = cranges[ncranges - 1 - m].minval;
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/*
+	 * Sort the values using the comparator function, and form ranges
+	 * from the sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		cranges[i].minval = values[2*i];
+		cranges[i].maxval = values[2*i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		cranges[i].collapsed = (compare_values(&values[2*i],
+											   &values[2*i+1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+
 /*
  * reduce_combine_ranges
  *		reduce the ranges until the number of values is low enough
@@ -1366,6 +1433,14 @@ reduce_combine_ranges(CombineRange *cranges, int ncranges,
 	if (keep >= ndistances)
 		return ncranges;
 
+	/*
+	 * Without distances, we use a simple approach keeping as many
+	 * outliers as possible.
+	 */
+	if (!distances)
+		return reduce_combine_ranges_simple(cranges, ncranges, max_values,
+											cmp, colloid);
+
 	/* sort the values */
 	cxt.colloid = colloid;
 	cxt.cmpFn = cmp;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 8135854163..5cf820f04b 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,23 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# minmax multi bytea
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '<(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '2', amopopr => '<=(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '3', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '4', amopopr => '>=(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # bloom bytea
 { amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
@@ -1836,6 +1853,23 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# minmax multi "char"
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '2', amopopr => '<=(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '3', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '4', amopopr => '>=(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
+  amopmethod => 'brin' },
+
 # bloom "char"
 { amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
@@ -1858,6 +1892,23 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# minmax multi name
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '2', amopopr => '<=(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '3', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '4', amopopr => '>=(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
+  amopmethod => 'brin' },
+
 # bloom name
 { amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
@@ -2186,6 +2237,23 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# minmax multi text
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '2', amopopr => '<=(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '3', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '4', amopopr => '>=(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
+  amopmethod => 'brin' },
+
 # bloom text
 { amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
@@ -2562,6 +2630,23 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# minmax multi character
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '<(bpchar,bpchar)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '2',
+  amopopr => '<=(bpchar,bpchar)', amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '3', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '4',
+  amopopr => '>=(bpchar,bpchar)', amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # bloom character
 { amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
   amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
@@ -3007,6 +3092,23 @@
   amoprighttype => 'bit', amopstrategy => '5', amopopr => '>(bit,bit)',
   amopmethod => 'brin' },
 
+# minmax multi bit
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '2', amopopr => '<=(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '3', amopopr => '=(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '4', amopopr => '>=(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '5', amopopr => '>(bit,bit)',
+  amopmethod => 'brin' },
+
 # minmax bit varying
 { amopfamily => 'brin/varbit_minmax_ops', amoplefttype => 'varbit',
   amoprighttype => 'varbit', amopstrategy => '1', amopopr => '<(varbit,varbit)',
@@ -3024,6 +3126,23 @@
   amoprighttype => 'varbit', amopstrategy => '5', amopopr => '>(varbit,varbit)',
   amopmethod => 'brin' },
 
+# minmax multi bit varying
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '1', amopopr => '<(varbit,varbit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '2',
+  amopopr => '<=(varbit,varbit)', amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '3', amopopr => '=(varbit,varbit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '4',
+  amopopr => '>=(varbit,varbit)', amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '5', amopopr => '>(varbit,varbit)',
+  amopmethod => 'brin' },
+
 # minmax numeric
 { amopfamily => 'brin/numeric_minmax_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 51403716b1..69a7ed682c 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,22 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi bytea
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom bytea
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '1',
@@ -836,6 +852,22 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi "char"
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom "char"
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -867,6 +899,22 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi name
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom name
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -1197,6 +1245,22 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi text
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom text
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -1663,6 +1727,23 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi character
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom character
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '1',
@@ -2180,6 +2261,21 @@
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi bit
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # minmax bit varying
 { amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
   amprocrighttype => 'varbit', amprocnum => '1',
@@ -2194,6 +2290,23 @@
   amprocrighttype => 'varbit', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi bit varying
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # minmax numeric
 { amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index da25befefe..823f1b01fe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,18 +266,27 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_minmax_multi_ops',
+  opcfamily => 'brin/bytea_minmax_multi_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bytea_bloom_ops',
   opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
   opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_minmax_multi_ops',
+  opcfamily => 'brin/char_minmax_multi_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_bloom_ops',
   opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
   opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_minmax_multi_ops',
+  opcfamily => 'brin/name_minmax_multi_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_bloom_ops',
   opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
   opckeytype => 'name', opcdefault => 'f' },
@@ -311,6 +320,9 @@
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_minmax_multi_ops',
+  opcfamily => 'brin/text_minmax_multi_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_bloom_ops',
   opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
   opckeytype => 'text', opcdefault => 'f' },
@@ -381,6 +393,9 @@
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_minmax_multi_ops',
+  opcfamily => 'brin/bpchar_minmax_multi_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
   opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar', opcdefault => 'f' },
@@ -440,9 +455,15 @@
   opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
+{ opcmethod => 'brin', opcname => 'bit_minmax_multi_ops',
+  opcfamily => 'brin/bit_minmax_multi_ops', opcintype => 'bit',
+  opckeytype => 'bit', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
   opcfamily => 'brin/varbit_minmax_ops', opcintype => 'varbit',
   opckeytype => 'varbit' },
+{ opcmethod => 'brin', opcname => 'varbit_minmax_multi_ops',
+  opcfamily => 'brin/varbit_minmax_multi_ops', opcintype => 'varbit',
+  opckeytype => 'varbit', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index ba9231ac8c..ffb20e72ab 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -192,6 +192,8 @@
   opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '9967',
+  opfmethod => 'brin', opfname => 'text_minmax_multi_ops' },
 { oid => '9902',
   opfmethod => 'brin', opfname => 'text_bloom_ops' },
 { oid => '9903',
@@ -210,14 +212,20 @@
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '9968',
+  opfmethod => 'brin', opfname => 'char_minmax_multi_ops' },
 { oid => '9906',
   opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '9969',
+  opfmethod => 'brin', opfname => 'bytea_minmax_multi_ops' },
 { oid => '9907',
   opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '9970',
+  opfmethod => 'brin', opfname => 'name_minmax_multi_ops' },
 { oid => '9908',
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
@@ -260,6 +268,8 @@
   opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '9971',
+  opfmethod => 'brin', opfname => 'bpchar_minmax_multi_ops' },
 { oid => '9915',
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
@@ -276,8 +286,12 @@
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
+{ oid => '9972',
+  opfmethod => 'brin', opfname => 'bit_minmax_multi_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
+{ oid => '9973',
+  opfmethod => 'brin', opfname => 'varbit_minmax_multi_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
 { oid => '9938',
-- 
2.26.2

0007-Remove-the-special-batch-mode-use-a-larger--20210211.patchtext/x-patch; charset=UTF-8; name=0007-Remove-the-special-batch-mode-use-a-larger--20210211.patchDownload
From 3133cf12a6aac2c9510e41c9ec0fe8df53b92b6e Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Tue, 2 Feb 2021 01:57:54 +0100
Subject: [PATCH 7/9] Remove the special batch mode, use a larger buffer always

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-specified size). That should give
us most of the benefits without consuming a lot of memory.
---
 src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++--------
 1 file changed, 525 insertions(+), 326 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 69a72da337..08d0d55b06 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -92,7 +92,15 @@
  */
 #define		PROCNUM_BASE			11
 
-#define		MINMAX_LOAD_FACTOR		0.75
+/*
+ * Sizing the insert buffer - we use 10x the number of values specified
+ * in the reloption, but we cap it to 8192 not to get too large. When
+ * the buffer gets full, we reduce the number of values by half.
+ */
+#define		MINMAX_BUFFER_FACTOR			10
+#define		MINMAX_BUFFER_MIN				256
+#define		MINMAX_BUFFER_MAX				8192
+#define		MINMAX_BUFFER_LOAD_FACTOR		0.5
 
 typedef struct MinmaxMultiOpaque
 {
@@ -155,23 +163,24 @@ typedef struct Ranges
 	Oid			typid;
 	Oid			colloid;
 	AttrNumber	attno;
+	FmgrInfo   *cmp;
 
 	/* (2*nranges + nvalues) <= maxvalues */
 	int		nranges;	/* number of ranges in the array (stored) */
+	int		nsorted;	/* number of sorted values (ranges + points) */
 	int		nvalues;	/* number of values in the data array (all) */
 	int		maxvalues;	/* maximum number of values (reloption) */
 
 	/*
-	 * In batch mode, we simply add the values into a buffer, without any
-	 * expensive steps (sorting, deduplication, ...). The buffer is sized
-	 * to be larger than the target number of values per range, which
-	 * reduces the number of compactions - operating on larger buffers is
-	 * significantly more efficient, in most cases. We keep the actual
-	 * target and compact to the requested number of values at the very
-	 * end, before serializing to on-disk representation.
+	 * We simply add the values into a large buffer, without any expensive
+	 * steps (sorting, deduplication, ...). The buffer is a multiple of
+	 * the target number of values, so the compaction happen less often,
+	 * amortizing the costs. We keep the actual target and compact to
+	 * the requested number of values at the very end, before serializing
+	 * to on-disk representation.
 	 */
-	bool	batch_mode;
-	int		target_maxvalues;	/* requested number of values */
+	/* requested number of values */
+	int		target_maxvalues;
 
 	/* values stored for this range - either raw values, or ranges */
 	Datum	values[FLEXIBLE_ARRAY_MEMBER];
@@ -203,7 +212,7 @@ typedef struct SerializedRanges
 
 static SerializedRanges *range_serialize(Ranges *range);
 
-static Ranges *range_deserialize(SerializedRanges *range);
+static Ranges *range_deserialize(int maxvalues, SerializedRanges *range);
 
 /* Cache for support and strategy procesures. */
 
@@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
 static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
 					   uint16 attno, Oid subtype, uint16 strategynum);
 
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+static int compare_values(const void *a, const void *b, void *arg);
+
 
 /*
  * minmax_multi_init
@@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues)
 	return ranges;
 }
 
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid);
+
+
+static void
+range_deduplicate_values(Ranges *range)
+{
+	int				i, n;
+	int				start;
+	compare_context cxt;
+
+	/*
+	 * If there are no unsorted values, we're done (this probably can't
+	 * happen, as we're adding values to unsorted part).
+	 */
+	if (range->nsorted == range->nvalues)
+		return;
+
+	/* sort the values */
+	cxt.colloid = range->colloid;
+	cxt.cmpFn = range->cmp;
+
+	/* how many values to sort? */
+	start = 2 * range->nranges;
+
+	qsort_arg(&range->values[start],
+			  range->nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	n = 1;
+	for (i = 1; i < range->nvalues; i++)
+	{
+		/* same as preceding value, so store it */
+		if (compare_values(&range->values[start + i - 1],
+						   &range->values[start + i],
+						   (void *) &cxt) == 0)
+			continue;
+
+		range->values[start + n] = range->values[start + i];
+
+		n++;
+	}
+
+	/* now all the values are sorted */
+	range->nvalues = n;
+	range->nsorted = n;
+
+	AssertCheckRanges(range, range->cmp, range->colloid);
+}
+
+
 /*
  * range_serialize
  *	  Serialize the in-memory representation into a compact varlena value.
@@ -262,14 +330,25 @@ range_serialize(Ranges *range)
 
 	/* simple sanity checks */
 	Assert(range->nranges >= 0);
+	Assert(range->nsorted >= 0);
 	Assert(range->nvalues >= 0);
 	Assert(range->maxvalues > 0);
+	Assert(range->target_maxvalues > 0);
+
+	/* at this point the range should be compacted to the target size */
+	Assert(2*range->nranges + range->nvalues <= range->target_maxvalues);
+
+	Assert(range->target_maxvalues <= range->maxvalues);
+
+	/* range boundaries are always sorted */
+	Assert(range->nvalues >= range->nsorted);
+
+	/* sort and deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
 
 	/* see how many Datum values we actually have */
 	nvalues = 2*range->nranges + range->nvalues;
 
-	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
-
 	typid = range->typid;
 	typbyval = get_typbyval(typid);
 	typlen = get_typlen(typid);
@@ -316,7 +395,7 @@ range_serialize(Ranges *range)
 	serialized->typid = typid;
 	serialized->nranges = range->nranges;
 	serialized->nvalues = range->nvalues;
-	serialized->maxvalues = range->maxvalues;
+	serialized->maxvalues = range->target_maxvalues;
 
 	/*
 	 * And now copy also the boundary values (like the length calculation
@@ -367,7 +446,7 @@ range_serialize(Ranges *range)
  * in the in-memory value array.
  */
 static Ranges *
-range_deserialize(SerializedRanges *serialized)
+range_deserialize(int maxvalues, SerializedRanges *serialized)
 {
 	int		i,
 			nvalues;
@@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized)
 	nvalues = 2*serialized->nranges + serialized->nvalues;
 
 	Assert(nvalues <= serialized->maxvalues);
+	Assert(serialized->maxvalues <= maxvalues);
 
-	range = minmax_multi_init(serialized->maxvalues);
+	range = minmax_multi_init(maxvalues);
 
 	/* copy the header info */
 	range->nranges = serialized->nranges;
 	range->nvalues = serialized->nvalues;
-	range->maxvalues = serialized->maxvalues;
+	range->nsorted = serialized->nvalues;
+	range->maxvalues = maxvalues;
+	range->target_maxvalues = serialized->maxvalues;
+
 	range->typid = serialized->typid;
-	range->batch_mode = false;
 
 	typbyval = get_typbyval(serialized->typid);
 	typlen = get_typlen(serialized->typid);
@@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized)
 	return range;
 }
 
-typedef struct compare_context
-{
-	FmgrInfo   *cmpFn;
-	Oid			colloid;
-} compare_context;
-
 /*
  * Used to represent ranges expanded during merging and combining (to
  * reduce number of boundary values to store).
@@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg)
 	return 0;
 }
 
+void *bsearch_arg(const void *key, const void *base,
+						 size_t nmemb, size_t size,
+						 int (*compar) (const void *, const void *, void *),
+						 void *arg);
+
+static bool
+has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges,
+				   Datum newval, AttrNumber attno, Oid typid)
+{
+	Datum	compar;
+
+	Datum	minvalue = ranges->values[0];
+	Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+	FmgrInfo *cmpLessFn;
+	FmgrInfo *cmpGreaterFn;
+
+	/* binary search on ranges */
+	int		start,
+			end;
+
+	if (ranges->nranges == 0)
+		return false;
+
+	/*
+	 * Otherwise, need to compare the new value with boundaries of all
+	 * the ranges. First check if it's less than the absolute minimum,
+	 * which is the first value in the array.
+	 */
+	cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+										 BTLessStrategyNumber);
+	compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+	/* smaller than the smallest value in the range list */
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * And now compare it to the existing maximum (last value in the
+	 * data array). But only if we haven't already ruled out a possible
+	 * match in the minvalue check.
+	 */
+	cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+										BTGreaterStrategyNumber);
+	compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * So we know it's in the general min/max, the question is whether it
+	 * falls in one of the ranges or gaps. We'll use a binary search on
+	 * the ranges.
+	 *
+	 * it's in the general range, but is it actually covered by any
+	 * of the ranges? Repeat the check for each range.
+	 *
+	 * XXX We simply walk the ranges sequentially, but maybe we could
+	 * further leverage the ordering and non-overlap and use bsearch to
+	 * speed this up a bit.
+	 */
+	start = 0;					/* first range */
+	end = ranges->nranges - 1;	/* last range */
+	while (true)
+	{
+		int		midpoint = (start + end) / 2;
+
+		/* this means we ran out of ranges in the last step */
+		if (start > end)
+			return false;
+
+		/* copy the min/max values from the ranges */
+		minvalue = ranges->values[2 * midpoint];
+		maxvalue = ranges->values[2 * midpoint + 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse
+		 * to the left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in this range */
+		if (DatumGetBool(compar))
+		{
+			end = (midpoint - 1);
+			continue;
+		}
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse
+		 * to the right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+		/* larger than the largest value in this range */
+		if (DatumGetBool(compar))
+		{
+			start = (midpoint + 1);
+			continue;
+		}
+
+		/* hey, we found a matching range */
+		return true;
+	}
+
+	return false;
+}
+
+
 /*
  * range_contains_value
  * 		See if the new value is already contained in the range list.
@@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid,
 							Ranges *ranges, Datum newval)
 {
 	int			i;
-	FmgrInfo   *cmpLessFn;
-	FmgrInfo   *cmpGreaterFn;
 	FmgrInfo   *cmpEqualFn;
 	Oid			typid = attr->atttypid;
 
@@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid,
 	 * range, and only when there's still a chance of getting a match we
 	 * inspect the individual ranges.
 	 */
-	if (ranges->nranges > 0)
-	{
-		Datum	compar;
-		bool	match = true;
-
-		Datum	minvalue = ranges->values[0];
-		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
-
-		/*
-		 * Otherwise, need to compare the new value with boundaries of all
-		 * the ranges. First check if it's less than the absolute minimum,
-		 * which is the first value in the array.
-		 */
-		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
-											 BTLessStrategyNumber);
-		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
-
-		/* smaller than the smallest value in the range list */
-		if (DatumGetBool(compar))
-			match = false;
-
-		/*
-		 * And now compare it to the existing maximum (last value in the
-		 * data array). But only if we haven't already ruled out a possible
-		 * match in the minvalue check.
-		 */
-		if (match)
-		{
-			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
-												BTGreaterStrategyNumber);
-			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
-
-			if (DatumGetBool(compar))
-				match = false;
-		}
-
-		/*
-		 * So it's in the general range, but is it actually covered by any
-		 * of the ranges? Repeat the check for each range.
-		 *
-		 * XXX We simply walk the ranges sequentially, but maybe we could
-		 * further leverage the ordering and non-overlap and use bsearch to
-		 * speed this up a bit.
-		 */
-		for (i = 0; i < ranges->nranges && match; i++)
-		{
-			/* copy the min/max values from the ranges */
-			minvalue = ranges->values[2*i];
-			maxvalue = ranges->values[2*i+1];
-
-			/*
-			 * Otherwise, need to compare the new value with boundaries of all
-			 * the ranges. First check if it's less than the absolute minimum,
-			 * which is the first value in the array.
-			 */
-			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
-
-			/* smaller than the smallest value in this range */
-			if (DatumGetBool(compar))
-				continue;
-
-			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
-
-			/* larger than the largest value in this range */
-			if (DatumGetBool(compar))
-				continue;
-
-			/* hey, we found a matching row */
-			return true;
-		}
-	}
+	if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid))
+		return true;
 
 	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
 											 BTEqualStrategyNumber);
@@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid,
 	/*
 	 * We're done with the ranges, now let's inspect the exact values.
 	 *
-	 * XXX Again, we do sequentially search the values - consider leveraging
-	 * the ordering of values to improve performance.
+	 * XXX We do sequential search for small number of values, and bsearch
+	 * once we have more than 16 values.
+	 *
+	 * XXX We only inspect the sorted part - that's OK. For building it may
+	 * produce false negatives, but only after we already added some values
+	 * to the unsorted part, so we've modified the value. And when querying
+	 * the index, there should be no unsorted values.
 	 */
-	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	if (ranges->nsorted >= 16)
 	{
-		Datum compar;
+		compare_context	cxt;
 
-		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
 
-		/* found an exact match */
-		if (DatumGetBool(compar))
+		if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) != NULL)
 			return true;
 	}
-
-	/* the value is not covered by this BRIN tuple */
-	return false;
-}
-
-/*
- * insert_value
- *	  Adds a new value into the single-point part, while maintaining ordering.
- *
- * The function inserts the new value to the right place in the single-point
- * part of the range. It assumes there's enough free space, and then does
- * essentially an insert-sort.
- *
- * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
- * only the first nvalues are used.
- */
-static void
-insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
-			 Datum newvalue)
-{
-	int	i;
-	Datum	lt;
-
-	/* If there are no values yet, store the new one and we're done. */
-	if (!nvalues)
+	else
 	{
-		values[0] = newvalue;
-		return;
-	}
-
-	/*
-	 * A common case is that the new value is entirely out of the existing
-	 * range, i.e. it's either smaller or larger than all previous values.
-	 * So we check and handle this case first - first we check the larger
-	 * case, because in that case we can just append the value to the end
-	 * of the array and we're done.
-	 */
+		for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++)
+		{
+			Datum compar;
 
-	/* Is it greater than all existing values in the array? */
-	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
-	if (DatumGetBool(lt))
-	{
-		/* just copy it in-place and we're done */
-		values[nvalues] = newvalue;
-		return;
-	}
+			compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
 
-	/*
-	 * OK, I lied a bit - we won't check the smaller case explicitly, but
-	 * we'll just compare the value to all existing values in the array.
-	 * But we happen to start with the smallest value, so we're actually
-	 * doing the check anyway.
-	 *
-	 * XXX We do walk the values sequentially. Perhaps we could/should be
-	 * smarter and do some sort of bisection, to improve performance?
-	 */
-	for (i = 0; i < nvalues; i++)
-	{
-		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
-		if (DatumGetBool(lt))
-		{
-			/*
-			 * Move values to make space for the new entry, which should go
-			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
-			 */
-			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
-			values[i] = newvalue;
-			return;
+			/* found an exact match */
+			if (DatumGetBool(compar))
+				return true;
 		}
 	}
 
-	/* We should never really get here. */
-	Assert(false);
+	/* the value is not covered by this BRIN tuple */
+	return false;
 }
 
 #ifdef USE_ASSERT_CHECKING
@@ -754,11 +818,12 @@ static void
 AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
 {
 #ifdef USE_ASSERT_CHECKING
-	int i, j;
+	int i;
 
 	/* some basic sanity checks */
 	Assert(ranges->nranges >= 0);
-	Assert(ranges->nvalues >= 0);
+	Assert(ranges->nsorted >= 0);
+	Assert(ranges->nvalues >= ranges->nsorted);
 	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
 	Assert(ranges->typid != InvalidOid);
 
@@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
 	 */
 	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
 
-	/* finally check that none of the values are not covered by ranges */
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nsorted);
+
+	/*
+	 * Check that none of the values are not covered by ranges (both
+	 * sorted and unsorted)
+	 */
 	for (i = 0; i < ranges->nvalues; i++)
 	{
+		Datum	compar;
+		int		start,
+				end;
+		Datum	minvalue,
+				maxvalue;
+
 		Datum	value = ranges->values[2 * ranges->nranges + i];
 
-		for (j = 0; j < ranges->nranges; j++)
+		if (ranges->nranges == 0)
+			break;
+
+		minvalue = ranges->values[0];
+		maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse
+		 * to the left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+		/* smaller than the smallest value in the first range */
+		if (DatumGetBool(compar))
+			continue;
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse
+		 * to the right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+		/* larger than the largest value in the last range */
+		if (DatumGetBool(compar))
+			continue;
+
+		start = 0;					/* first range */
+		end = ranges->nranges - 1;	/* last range */
+		while (true)
 		{
-			Datum	r;
+			int		midpoint = (start + end) / 2;
+
+			/* this means we ran out of ranges in the last step */
+			if (start > end)
+				break;
+
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2 * midpoint];
+			maxvalue = ranges->values[2 * midpoint + 1];
 
-			Datum	minval = ranges->values[2 * j];
-			Datum	maxval = ranges->values[2 * j + 1];
+			/*
+			 * Is the value smaller than the minval? If yes, we'll recurse
+			 * to the left side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
 
-			/* if value is smaller than range minimum, that's OK */
-			r = FunctionCall2Coll(cmpFn, colloid, value, minval);
-			if (DatumGetBool(r))
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+			{
+				end = (midpoint - 1);
 				continue;
+			}
+
+			/*
+			 * Is the value greater than the minval? If yes, we'll recurse
+			 * to the right side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
 
-			/* if value is greater than range maximum, that's OK */
-			r = FunctionCall2Coll(cmpFn, colloid, maxval, value);
-			if (DatumGetBool(r))
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+			{
+				start = (midpoint + 1);
 				continue;
+			}
 
-			/* value is between [min,max], which is wrong */
+			/* hey, we found a matching range */
 			Assert(false);
 		}
 	}
+
+	/* and values in the unsorted part must not be in sorted part */
+	for (i = ranges->nsorted; i < ranges->nvalues; i++)
+	{
+		compare_context	cxt;
+		Datum	value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nsorted == 0)
+			break;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) == NULL);
+	}
 #endif
 }
 
@@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid,
  */
 static CombineRange *
 build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
-					 bool addvalue, Datum newvalue, int *nranges,
-					 bool deduplicate)
+					 int *nranges)
 {
 	int				ncranges;
 	CombineRange   *cranges;
@@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
 	/* now do the actual merge sort */
 	ncranges = ranges->nranges + ranges->nvalues;
 
-	/* should we add an extra value? */
-	if (addvalue)
-		ncranges += 1;
-
 	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
 
-	/* put the new value at the beginning */
-	if (addvalue)
-	{
-		cranges[0].minval = newvalue;
-		cranges[0].maxval = newvalue;
-		cranges[0].collapsed = true;
-
-		/* then the regular and collapsed ranges */
-		fill_combine_ranges(&cranges[1], ncranges-1, ranges);
-	}
-	else
-		fill_combine_ranges(cranges, ncranges, ranges);
+	/* fll the combine ranges */
+	fill_combine_ranges(cranges, ncranges, ranges);
 
 	/* and sort the ranges */
-	ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges,
-								   deduplicate);
+	ncranges = sort_combine_ranges(cmp, colloid,
+								   cranges, ncranges,
+								   true);	/* deduplicate */
 
 	/* remember how many cranges we built */
 	*nranges = ncranges;
@@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
 		}
 	}
 
+	/* all the values are sorted */
+	ranges->nsorted = ranges->nvalues;
+
 	Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues);
 	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
 }
 
+
+
 /*
- * range_add_value
- * 		Add the new value to the multi-minmax range.
+ * Consider freeing space in the ranges.
+ *
+ * Returns true if the value was actually modified.
  */
 static bool
-range_add_value(BrinDesc *bdesc, Oid colloid,
-				AttrNumber attno, Form_pg_attribute attr,
-				Ranges *ranges, Datum newval)
+ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *range)
 {
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
 	FmgrInfo   *cmpFn,
 			   *distanceFn;
 
@@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	int				ncranges;
 	DistanceValue  *distances;
 
-	MemoryContext	ctx;
-	MemoryContext	oldctx;
-
-	/* we'll certainly need the comparator, so just look it up now */
-	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
-											   BTLessStrategyNumber);
-
-	/* comprehensive checks of the input ranges */
-	AssertCheckRanges(ranges, cmpFn, colloid);
-
-	Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0));
-
 	/*
-	 * When batch-building, there should be no ranges. So either the
-	 * number of ranges is 0 or we're not in batching mode.
+	 * If there is free space in the buffer, we're done without having
+	 * to modify anything.
 	 */
-	Assert(!ranges->batch_mode || (ranges->nranges == 0));
-
-	/* When batch-building, just add it and we're done. */
-	if (ranges->batch_mode)
-	{
-		/* there has to be free space, if we've sized the struct */
-		Assert(ranges->nvalues < ranges->maxvalues);
-
-		/* Make a copy of the value, if needed. */
-		ranges->values[ranges->nvalues++]
-			= datumCopy(newval, attr->attbyval, attr->attlen);;
-
-		return true;
-	}
-
-	/*
-	 * Bail out if the value already is covered by the range.
-	 *
-	 * We could also add values until we hit values_per_range, and then
-	 * do the deduplication in a batch, hoping for better efficiency. But
-	 * that would mean we actually modify the range every time, which means
-	 * having to serialize the value, which does palloc, walks the values,
-	 * copies them, etc. Not exactly cheap.
-	 *
-	 * So instead we do the check, which should be fairly cheap - assuming
-	 * the comparator function is not very expensive.
-	 *
-	 * This also implies means the values array can't contain duplicities.
-	 */
-	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+	if (2*range->nranges + range->nvalues < range->maxvalues)
 		return false;
 
-	/* Make a copy of the value, if needed. */
-	newval = datumCopy(newval, attr->attbyval, attr->attlen);
-
-	/*
-	 * If there's space in the values array, copy it in and we're done.
-	 *
-	 * We do want to keep the values sorted (to speed up searches), so we
-	 * do a simple insertion sort. We could do something more elaborate,
-	 * e.g. by sorting the values only now and then, but for small counts
-	 * (e.g. when maxvalues is 64) this should be fine.
-	 */
-	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
-	{
-		Datum	   *values;
-
-		/* beginning of the 'single value' part (for convenience) */
-		values = &ranges->values[2*ranges->nranges];
-
-		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
-
-		ranges->nvalues++;
-
-		/*
-		 * Check we haven't broken the ordering of boundary values (checks
-		 * both parts, but that doesn't hurt).
-		 */
-		AssertCheckRanges(ranges, cmpFn, colloid);
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
 
-		/* Also check the range contains the value we just added. */
-		// FIXME Assert(ranges, cmpFn, colloid);
+	/* Try deduplicating values in the unsorted part */
+	range_deduplicate_values(range);
 
-		/* yep, we've modified the range */
+	/* did we reduce enough free space by just the deduplication? */
+	if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR)
 		return true;
-	}
 
 	/*
-	 * Damn - the new value is not in the range yet, but we don't have space
-	 * to just insert it. So we need to combine some of the existing ranges,
-	 * to reduce the number of values we need to store (joining two intervals
-	 * reduces the number of boundaries to store by 2).
+	 * we need to combine some of the existing ranges, to reduce the number
+	 * of values we need to store (joining intervals reduces the number of
+	 * boundary values).
 	 *
-	 * To do that we first construct an array of CombineRange items - each
-	 * combine range tracks if it's a regular range or collapsed range, where
-	 * "collapsed" means "single point."
+	 * We first construct an array of CombineRange items - each combine range
+	 * tracks if it's a regular range or a collapsed range, where "collapsed"
+	 * means "single point." This makes the processing easier, as it allows
+	 * handling ranges and points the same way.
 	 *
-	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
-	 * directly, while single points (ranges->nvalues of them) have to be
-	 * expanded. We neet the combine ranges to be sorted, and we do that by
-	 * performing a merge sort of ranges, values and new value.
+	 * Then we sort the combine ranges - this is necessary, because although
+	 * ranges and points were sorted on their own, the new array is not. We
+	 * do that by performing a merge sort of the two parts.
 	 *
 	 * The distanceFn calls (which may internally call e.g. numeric_le) may
-	 * allocate quite a bit of memory, and we must not leak it. Otherwise
-	 * we'd have problems e.g. when building indexes. So we create a local
-	 * memory context and make sure we free the memory before leaving this
-	 * function (not after every call).
+	 * allocate quite a bit of memory, and we must not leak it (we might have
+	 * to do this repeatedly, even for a single BRIN page range). Otherwise
+	 * we'd have problems e.g. when building new indexes. So we use a memory
+	 * context and make sure we free the memory at the end (so if we call
+	 * the distance function many times, it might be an issue, but meh).
 	 */
 	ctx = AllocSetContextCreate(CurrentMemoryContext,
 								"minmax-multi context",
@@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	oldctx = MemoryContextSwitchTo(ctx);
 
 	/* OK build the combine ranges */
-	cranges = build_combine_ranges(cmpFn, colloid, ranges,
-								   true, newval, &ncranges,
-								   false);
+	cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges);
 
 	/* and we'll also need the 'distance' procedure */
 	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
@@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	 * use too low or high value.
 	 */
 	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
-									 ranges->maxvalues * MINMAX_LOAD_FACTOR,
+									 range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR,
 									 cmpFn, colloid);
 
-	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR);
+	Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR);
 
 	/* decompose the combine ranges into regular ranges and single values */
-	store_combine_ranges(ranges, cranges, ncranges);
+	store_combine_ranges(range, cranges, ncranges);
 
 	MemoryContextSwitchTo(oldctx);
 	MemoryContextDelete(ctx);
 
 	/* Did we break the ranges somehow? */
+	AssertCheckRanges(range, cmpFn, colloid);
+
+	return true;
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn;
+	bool		modified = false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
 	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Make sure there's enough free space in the buffer. We only trigger
+	 * this when the buffer is full, which means it had to be modified as
+	 * we size it to be larger than what is stored on disk.
+	 *
+	 * XXX This needs to happen before we check if the value is contained
+	 * in the range, because the value might be in the unsorted part, and
+	 * we don't check that in range_contains_value. The deduplication would
+	 * then move it to the sorted part, and we'd add the value too, which
+	 * violates the rule that we never have duplicates with the ranges
+	 * or sorted values.
+	 *
+	 * XXX At the moment this only does the deduplication.
+	 *
+	 * XXX We might also deduplicate and recheck if the value is contained,
+	 * but that seems like an overkill. We'd need to deduplicate anyway,
+	 * so why not do it now.
+	 */
+	modified = ensure_free_space_in_buffer(bdesc, colloid,
+										   attno, attr, ranges);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return modified;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	ranges->values[2*ranges->nranges + ranges->nvalues] = newval;
+	ranges->nvalues++;
+
+	/*
+	 * Check we haven't broken the ordering of boundary values (checks
+	 * both parts, but that doesn't hurt).
+	 */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/* Also check the range contains the value we just added. */
 	// FIXME Assert(ranges, cmpFn, colloid);
 
+	/* yep, we've modified the range */
 	return true;
 }
 
@@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
 	MemoryContext	ctx;
 	MemoryContext	oldctx;
 
-	/*
-	 * This should only be used in batch mode, and there should be no
-	 * ranges, just individual values.
-	 */
-	Assert((ranges->batch_mode) && (ranges->nranges == 0));
-
 	/* we'll certainly need the comparator, so just look it up now */
 	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
 											   BTLessStrategyNumber);
@@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
 
 	/* OK build the combine ranges */
 	cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges,
-								   false, (Datum) 0, &ncranges,
-								   true);	/* deduplicate */
+								   &ncranges);	/* deduplicate */
 
 	if (ncranges > 1)
 	{
@@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
 		 * don't expect more tuples to be inserted soon.
 		 */
 		ncranges = reduce_combine_ranges(cranges, ncranges, distances,
-										  max_values, cmpFn, ranges->colloid);
+										 max_values, cmpFn, ranges->colloid);
 
 		Assert(count_values(cranges, ncranges) <= max_values);
 	}
@@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
 	 * In batch mode, we need to compress the accumulated values to the
 	 * actually requested number of values/ranges.
 	 */
-	if (ranges->batch_mode)
-		compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+	compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
 
 	s = range_serialize(ranges);
 	dst[0] = PointerGetDatum(s);
@@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	{
 		MemoryContext oldctx;
 
+		int				target_maxvalues;
+		int				maxvalues;
 		BlockNumber		pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
 
+		/* what was specified as a reloption? */
+		target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped
+		 * to the maximum number of values in the heap range. This is more
+		 * than enough, considering the actual number of rows per page is
+		 * likely much lower, but meh.
+		 */
+		maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, target_maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
 		oldctx = MemoryContextSwitchTo(column->bv_context);
-		ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange);
+		ranges = minmax_multi_init(maxvalues);
 		ranges->attno = attno;
 		ranges->colloid = colloid;
 		ranges->typid = attr->atttypid;
-		ranges->batch_mode = true;
-		ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+		ranges->target_maxvalues = target_maxvalues;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
 
 		MemoryContextSwitchTo(oldctx);
 
@@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	{
 		MemoryContext oldctx;
 
+		int				maxvalues;
+		BlockNumber		pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
 		oldctx = MemoryContextSwitchTo(column->bv_context);
 
 		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-		ranges = range_deserialize(serialized);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped
+		 * to the maximum number of values in the heap range. This is more
+		 * than enough, considering the actual number of rows per page is
+		 * likely much lower, but meh.
+		 */
+		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, serialized->maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		ranges = range_deserialize(maxvalues, serialized);
+
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
 
 		column->bv_mem_value = PointerGetDatum(ranges);
 		column->bv_serialize = brin_minmax_multi_serialize;
@@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
 	attno = column->bv_attno;
 
 	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-	ranges = range_deserialize(serialized);
+	ranges = range_deserialize(serialized->maxvalues, serialized);
 
 	/* inspect the ranges, and for each one evaluate the scan keys */
 	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
@@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
 	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
 
-	ranges_a = range_deserialize(serialized_a);
-	ranges_b = range_deserialize(serialized_b);
+	ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a);
+	ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b);
 
 	/* make sure neither of the ranges is NULL */
 	Assert(ranges_a && ranges_b);
@@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 											 BTLessStrategyNumber);
 
-	/* sort the combine ranges (don't deduplicate) */
+	/* sort the combine ranges (no need to deduplicate) */
 	sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false);
 
 	/*
@@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
 	fmgr_info(outfunc, &fmgrinfo);
 
 	/* deserialize the range info easy-to-process pieces */
-	ranges_deserialized = range_deserialize(ranges);
+	ranges_deserialized = range_deserialize(ranges->maxvalues, ranges);
 
 	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
 					 ranges_deserialized->nranges,
-- 
2.26.2

0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patchtext/x-patch; charset=UTF-8; name=0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patchDownload
From ca329bfcf35ec94f6efd74be239773234bb2cd7c Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Tue, 2 Feb 2021 01:49:56 +0100
Subject: [PATCH 6/9] Batch mode when building new BRIN multi-minmax range

Strict enforcement of the values_per_range limit may be quite expensive,
particularly when adding many values into the same range, e.g. when
building a new index. This commit adds a "batch" mode which allows the
buffer to be much larger, so that the compaction happens only once at
the very end, as part of summary serialization. This amortizes the cost
as it's much more efficient to sort many values once.
---
 src/backend/access/brin/brin_minmax_multi.c | 227 ++++++++++++++++++--
 1 file changed, 204 insertions(+), 23 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 2f6e1793dc..69a72da337 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -57,6 +57,7 @@
 #include "access/brin.h"
 #include "access/brin_internal.h"
 #include "access/brin_tuple.h"
+#include "access/hash.h"	/* XXX strange that it fails because of BRIN_AM_OID without this */
 #include "access/reloptions.h"
 #include "access/stratnum.h"
 #include "access/htup_details.h"
@@ -150,13 +151,28 @@ typedef struct MinMaxOptions
  */
 typedef struct Ranges
 {
-	Oid		typid;
+	/* Cache information that we need quite often. */
+	Oid			typid;
+	Oid			colloid;
+	AttrNumber	attno;
 
 	/* (2*nranges + nvalues) <= maxvalues */
 	int		nranges;	/* number of ranges in the array (stored) */
 	int		nvalues;	/* number of values in the data array (all) */
 	int		maxvalues;	/* maximum number of values (reloption) */
 
+	/*
+	 * In batch mode, we simply add the values into a buffer, without any
+	 * expensive steps (sorting, deduplication, ...). The buffer is sized
+	 * to be larger than the target number of values per range, which
+	 * reduces the number of compactions - operating on larger buffers is
+	 * significantly more efficient, in most cases. We keep the actual
+	 * target and compact to the requested number of values at the very
+	 * end, before serializing to on-disk representation.
+	 */
+	bool	batch_mode;
+	int		target_maxvalues;	/* requested number of values */
+
 	/* values stored for this range - either raw values, or ranges */
 	Datum	values[FLEXIBLE_ARRAY_MEMBER];
 } Ranges;
@@ -376,6 +392,7 @@ range_deserialize(SerializedRanges *serialized)
 	range->nvalues = serialized->nvalues;
 	range->maxvalues = serialized->maxvalues;
 	range->typid = serialized->typid;
+	range->batch_mode = false;
 
 	typbyval = get_typbyval(serialized->typid);
 	typlen = get_typlen(serialized->typid);
@@ -753,10 +770,6 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
 	 */
 	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
 
-	/* then the single-point ranges (with nvalues boundary values ) */
-	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
-					 ranges->nvalues);
-
 	/* finally check that none of the values are not covered by ranges */
 	for (i = 0; i < ranges->nvalues; i++)
 	{
@@ -880,12 +893,22 @@ fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
 
 /*
  * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ *
+ * Optionally, the cranges may be deduplicated (this matters in batch mode,
+ * where we simply append values, without checking for duplicates etc.).
+ *
+ * Returns the number of combine ranges.
  */
-static void
+static int
 sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
-					CombineRange *cranges, int ncranges)
+					CombineRange *cranges, int ncranges,
+					bool deduplicate)
 {
-	compare_context cxt;
+	int				n;
+	int				i;
+	compare_context	cxt;
+
+	Assert(ncranges > 0);
 
 	/* sort the values */
 	cxt.colloid = colloid;
@@ -893,6 +916,26 @@ sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
 
 	qsort_arg(cranges, ncranges, sizeof(CombineRange),
 			  compare_combine_ranges, (void *) &cxt);
+
+	if (!deduplicate)
+		return ncranges;
+
+	/* optionally deduplicate the ranges */
+	n = 1;
+	for (i = 1; i < ncranges; i++)
+	{
+		if (compare_combine_ranges(&cranges[i-1], &cranges[i], (void *) &cxt))
+		{
+			if (i != n)
+				memcpy(&cranges[n], &cranges[i], sizeof(CombineRange));
+
+			n++;
+		}
+	}
+
+	Assert((n > 0) && (n <= ncranges));
+
+	return n;
 }
 
 /*
@@ -1063,26 +1106,40 @@ build_distances(FmgrInfo *distanceFn, Oid colloid,
  */
 static CombineRange *
 build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
-					 Datum newvalue, int *nranges)
+					 bool addvalue, Datum newvalue, int *nranges,
+					 bool deduplicate)
 {
 	int				ncranges;
 	CombineRange   *cranges;
 
 	/* now do the actual merge sort */
-	ncranges = ranges->nranges + ranges->nvalues + 1;
+	ncranges = ranges->nranges + ranges->nvalues;
+
+	/* should we add an extra value? */
+	if (addvalue)
+		ncranges += 1;
+
 	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
-	*nranges = ncranges;
 
 	/* put the new value at the beginning */
-	cranges[0].minval = newvalue;
-	cranges[0].maxval = newvalue;
-	cranges[0].collapsed = true;
+	if (addvalue)
+	{
+		cranges[0].minval = newvalue;
+		cranges[0].maxval = newvalue;
+		cranges[0].collapsed = true;
 
-	/* then the regular and collapsed ranges */
-	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+		/* then the regular and collapsed ranges */
+		fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+	}
+	else
+		fill_combine_ranges(cranges, ncranges, ranges);
 
 	/* and sort the ranges */
-	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+	ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges,
+								   deduplicate);
+
+	/* remember how many cranges we built */
+	*nranges = ncranges;
 
 	return cranges;
 }
@@ -1295,6 +1352,27 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	/* comprehensive checks of the input ranges */
 	AssertCheckRanges(ranges, cmpFn, colloid);
 
+	Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0));
+
+	/*
+	 * When batch-building, there should be no ranges. So either the
+	 * number of ranges is 0 or we're not in batching mode.
+	 */
+	Assert(!ranges->batch_mode || (ranges->nranges == 0));
+
+	/* When batch-building, just add it and we're done. */
+	if (ranges->batch_mode)
+	{
+		/* there has to be free space, if we've sized the struct */
+		Assert(ranges->nvalues < ranges->maxvalues);
+
+		/* Make a copy of the value, if needed. */
+		ranges->values[ranges->nvalues++]
+			= datumCopy(newval, attr->attbyval, attr->attlen);;
+
+		return true;
+	}
+
 	/*
 	 * Bail out if the value already is covered by the range.
 	 *
@@ -1375,7 +1453,9 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	oldctx = MemoryContextSwitchTo(ctx);
 
 	/* OK build the combine ranges */
-	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+	cranges = build_combine_ranges(cmpFn, colloid, ranges,
+								   true, newval, &ncranges,
+								   false);
 
 	/* and we'll also need the 'distance' procedure */
 	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
@@ -1407,6 +1487,82 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	return true;
 }
 
+/*
+ * Generate range representation of data collected during "batch mode".
+ * This is similar to reduce_combine_ranges, except that we can't assume
+ * the values are sorted and there may be duplicate values.
+ */
+static void
+compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	/*
+	 * This should only be used in batch mode, and there should be no
+	 * ranges, just individual values.
+	 */
+	Assert((ranges->batch_mode) && (ranges->nranges == 0));
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
+											   BTLessStrategyNumber);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, ranges->attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges,
+								   false, (Datum) 0, &ncranges,
+								   true);	/* deduplicate */
+
+	if (ncranges > 1)
+	{
+		/* build array of gap distances and sort them in ascending order */
+		distances = build_distances(distanceFn, ranges->colloid, cranges, ncranges);
+
+		/*
+		 * Combine ranges until we get below max_values. We don't use any scale
+		 * factor, because this is used at the very end of "batch mode" and we
+		 * don't expect more tuples to be inserted soon.
+		 */
+		ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+										  max_values, cmpFn, ranges->colloid);
+
+		Assert(count_values(cranges, ncranges) <= max_values);
+	}
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	/* check all the range invariants */
+	AssertCheckRanges(ranges, cmpFn, ranges->colloid);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+}
+
 Datum
 brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 {
@@ -1890,7 +2046,16 @@ static void
 brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
 {
 	Ranges *ranges = (Ranges *) DatumGetPointer(src);
-	SerializedRanges *s = range_serialize(ranges);
+	SerializedRanges *s;
+
+	/*
+	 * In batch mode, we need to compress the accumulated values to the
+	 * actually requested number of values/ranges.
+	 */
+	if (ranges->batch_mode)
+		compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+
+	s = range_serialize(ranges);
 	dst[0] = PointerGetDatum(s);
 }
 
@@ -1933,15 +2098,31 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	/*
 	 * If this is the first non-null value, we need to initialize the range
 	 * list. Otherwise just extract the existing range list from BrinValues.
+	 *
+	 * When starting with an empty range, we assume this is a batch mode,
+	 * i.e. we size the buffer for the maximum possible number of items in
+	 * the range (based on range size and max number of items on a page).
+	 *
+	 * XXX This may require quite a bit of memory, so maybe we should use
+	 * some value in between. OTOH most tables will have much wider rows,
+	 * so the number of rows per page is much lower.
+	 *
+	 * XXX Maybe we should do this (using larger buffer) even when there
+	 * already is a summary?
 	 */
 	if (column->bv_allnulls)
 	{
 		MemoryContext oldctx;
 
-		oldctx = MemoryContextSwitchTo(column->bv_context);
+		BlockNumber		pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
 
-		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+		ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange);
+		ranges->attno = attno;
+		ranges->colloid = colloid;
 		ranges->typid = attr->atttypid;
+		ranges->batch_mode = true;
+		ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
 
 		MemoryContextSwitchTo(oldctx);
 
@@ -2227,8 +2408,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 											 BTLessStrategyNumber);
 
-	/* sort the combine ranges */
-	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+	/* sort the combine ranges (don't deduplicate) */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false);
 
 	/*
 	 * We've merged two different lists of ranges, so some of them may be
-- 
2.26.2

0005-BRIN-minmax-multi-indexes-20210211.patchtext/x-patch; charset=UTF-8; name=0005-BRIN-minmax-multi-indexes-20210211.patchDownload
From 9c56414ac9422a70d383a5040f989993fb7f4cc7 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Wed, 3 Feb 2021 19:00:00 +0100
Subject: [PATCH 5/9] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2579 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   18 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    8 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 ++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   85 +
 src/include/catalog/pg_type.dat             |    6 +
 src/test/regress/expected/brin_multi.out    |  445 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  397 +++
 21 files changed, 5041 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 940a0fd8fd..f8a82205d8 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -612,6 +612,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     8 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..2f6e1793dc
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2579 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+#define		MINMAX_LOAD_FACTOR		0.75
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0))
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, with a simple header
+ * with basic metadata, followed by the boundary values. It has a varlena
+ * header, so can be treated as varlena directly.
+ *
+ * See range_serialize/range_deserialize for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * compare_values
+ *	  Compare the values.
+ */
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum *da = (Datum *) a;
+	Datum *db = (Datum *) b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *da, *db);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *db, *da);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+}
+#endif
+
+/*
+ * Comprehensive check of the Ranges structure.
+ */
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i, j;
+
+	/* some basic sanity checks */
+	Assert(ranges->nranges >= 0);
+	Assert(ranges->nvalues >= 0);
+	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
+	Assert(ranges->typid != InvalidOid);
+
+	/*
+	 * First the ranges - there are 2*nranges boundary values, and the
+	 * values have to be strictly ordered (equal values would mean the
+	 * range is collapsed, and should be stored as a point). This also
+	 * guarantees that the ranges do not overlap.
+	 */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundary values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+
+	/* finally check that none of the values are not covered by ranges */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	value = ranges->values[2 * ranges->nranges + i];
+
+		for (j = 0; j < ranges->nranges; j++)
+		{
+			Datum	r;
+
+			Datum	minval = ranges->values[2 * j];
+			Datum	maxval = ranges->values[2 * j + 1];
+
+			/* if value is smaller than range minimum, that's OK */
+			r = FunctionCall2Coll(cmpFn, colloid, value, minval);
+			if (DatumGetBool(r))
+				continue;
+
+			/* if value is greater than range maximum, that's OK */
+			r = FunctionCall2Coll(cmpFn, colloid, maxval, value);
+			if (DatumGetBool(r))
+				continue;
+
+			/* value is between [min,max], which is wrong */
+			Assert(false);
+		}
+	}
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ * This is intentionally sorting the distances in descending order, i.e.
+ * the longer gaps will be at the front.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return 1;
+	else if (da->value > db->value)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (max-min) for pairs
+ * of consecutive ranges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the gap (between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index of the gap the distance is for */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/*
+	 * Sort the distances in descending order, so that the longest gaps
+	 * are at the front.
+	 */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue),
+			 compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+#endif
+
+/*
+ * reduce_combine_ranges
+ *		reduce the ranges until the number of values is low enough
+ *
+ * Combines ranges until the number of boundary values drops below the
+ * threshold specified by max_values. This happens by merging enough
+ * ranges by distance between them.
+ *
+ * Returns the number of result ranges.
+ *
+ * We simply use the global min/max and then add boundaries for enough
+ * largest gaps. Each gap adds 2 values, so we simply use (target/2-1)
+ * distances. Then we simply sort all the values - each two values are
+ * a boundary of a range (possibly collapsed).
+ *
+ * XXX Some of the ranges may be collapsed (i.e. the min/max values are
+ * equal), but we ignore that for now. We could repeat the process,
+ * adding a couple more gaps recursively.
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ *
+ * XXX Or maybe we could just handle this by using random value as a
+ * tie-break, or by adding random noise to the actual distance.
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values,
+					  FmgrInfo *cmp, Oid colloid)
+{
+	int i;
+	int		nvalues;
+	Datum  *values;
+
+	compare_context cxt;
+
+	/* total number of gaps between ranges */
+	int	ndistances = (ncranges - 1);
+
+	/* number of gaps to keep */
+	int keep = (max_values/2 - 1);
+
+	/*
+	 * Maybe we have sufficiently low number of ranges already?
+	 *
+	 * XXX This should happen before we actually do the expensive stuff
+	 * like sorting, so maybe this should be just an assert.
+	 */
+	if (keep >= ndistances)
+		return ncranges;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	/* add the global min/max values, from the first/last range */
+	values[nvalues++] = cranges[0].minval;
+	values[nvalues++] = cranges[ncranges-1].maxval;
+
+	/* add boundary values for enough gaps */
+	for (i = 0; i < keep; i++)
+	{
+		/* index of the gap between (index) and (index+1) ranges */
+		int index = distances[i].index;
+
+		Assert((index >= 0) && ((index+1) < ncranges));
+
+		/* add max from the preceding range, minval from the next one */
+		values[nvalues++] = cranges[index].maxval;
+		values[nvalues++] = cranges[index+1].minval;
+
+		Assert(nvalues <= max_values);
+	}
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/*
+	 * Sort the values using the comparator function, and form ranges
+	 * from the sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		cranges[i].minval = values[2*i];
+		cranges[i].maxval = values[2*i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		cranges[i].collapsed = (compare_values(&values[2*i],
+											   &values[2*i+1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+
+	Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues);
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertCheckRanges(ranges, cmpFn, colloid);
+
+		/* Also check the range contains the value we just added. */
+		// FIXME Assert(ranges, cmpFn, colloid);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 *
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues * MINMAX_LOAD_FACTOR,
+									 cmpFn, colloid);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Did we break the ranges somehow? */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+	// FIXME Assert(ranges, cmpFn, colloid);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	/*
+	 * We use the no-check variants here, because user-supplied values
+	 * may have (ip_posid == 0). See ItemPointerCompare.
+	 */
+	da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumberNoCheck(pa1);
+
+	da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumberNoCheck(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	float8		delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute approximate distance between two dates.
+ */
+Datum
+brin_minmax_multi_distance_date(PG_FUNCTION_ARGS)
+{
+	DateADT		dateVal1 = PG_GETARG_DATEADT(0);
+	DateADT		dateVal2 = PG_GETARG_DATEADT(1);
+
+	PG_RETURN_FLOAT8(dateVal1 - dateVal2);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	float8	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	float8	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+Datum
+brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS)
+{
+	float8	delta = 0;
+
+	Timestamp	dt1 = PG_GETARG_TIMESTAMP(0);
+	Timestamp	dt2 = PG_GETARG_TIMESTAMP(1);
+
+	if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
+		PG_RETURN_FLOAT8(0);
+
+	delta = dt2 - dt1;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	float8	delta = 0;
+
+	Interval   *ia = PG_GETARG_INTERVAL_P(0);
+	Interval   *ib = PG_GETARG_INTERVAL_P(1);
+	Interval   *result;
+
+	result = (Interval *) palloc(sizeof(Interval));
+
+	result->month = ib->month - ia->month;
+	/* overflow check copied from int4mi */
+	if (!SAMESIGN(ib->month, ia->month) &&
+		!SAMESIGN(result->month, ib->month))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->day = ib->day - ia->day;
+	if (!SAMESIGN(ib->day, ia->day) &&
+		!SAMESIGN(result->day, ib->day))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->time = ib->time - ia->time;
+	if (!SAMESIGN(ib->time, ia->time) &&
+		!SAMESIGN(result->time, ib->time))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	/*
+	 * We assume months have 31 days - we don't need to be precise, in
+	 * the worst case we'll build somewhat less efficient ranges.
+	 */
+	delta = (float8) (result->month * 31 + result->day);
+
+	/* convert to microseconds (just like the time part) */
+	delta = 24L * 3600L * delta;
+
+	/* and add the time part */
+	delta += result->time / (float8) 1000000.0;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	float8	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	float8	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	float8	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((float8) b->h - (float8) a->h);
+	delta /= 256;
+
+	delta += ((float8) b->g - (float8) a->g);
+	delta /= 256;
+
+	delta += ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8)b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	float8				delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		PG_RETURN_FLOAT8(1.0);
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (float8) addrb[i] - (float8) addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/*
+	 * If needed, reduce some of the ranges.
+	 *
+	 * XXX This may be fairly expensive, so maybe we should do it only when
+	 * it's actually needed (when we have too many ranges).
+	 */
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges,
+	 * and if needed combine as many off them to get below the threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX This does not apply the load factor, as we don't expect to
+	 * add more values to the range, so we prefer to keep as many ranges
+	 * as possible.
+	 *
+	 * XXX Can the maxvalues be different in the two ranges? Perhaps
+	 * we should use maximum of those?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues,
+									 cmpFn, colloid);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 8, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..bf8635d788 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,15 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				brdesc,
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -495,6 +504,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +588,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 8cc4e532e6..fdaff42722 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..87de94f397 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,11 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+/*
+ * The BRIN opclasses may register serialization callback, in case the on-disk
+ * and in-memory representations differ (e.g. for performance reasons).
+ */
+typedef void (*brin_serialize_callback_type) (BrinDesc *bdesc, Datum src, Datum *dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +32,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index e07dd83550..e2a04d9cb0 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 04d678f96a..8135854163 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2062,6 +2208,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2088,6 +2251,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2155,6 +2334,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2180,6 +2425,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2202,6 +2464,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2224,6 +2503,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2288,6 +2584,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2439,6 +2752,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2470,6 +2929,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2492,6 +2968,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2548,6 +3041,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2570,6 +3080,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2636,6 +3163,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 6709c8dfea..51403716b1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index dea9adaf98..ba9231ac8c 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9926',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '9901',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9927',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '9902',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9928',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '9904',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9929',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '9905',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9930',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '9909',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '9910',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9931',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9932',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '9911',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9933',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '9912',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9934',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '9913',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9935',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '9914',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9936',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '9916',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9937',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '9917',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9938',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '9918',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9939',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '9919',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b7432c65d5..dbf0ee13b2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8197,6 +8197,77 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9940', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9941', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9942', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9943', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9944', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9945', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9946', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9947', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9948', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9949', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9950', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9951', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9952', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9953', descr => 'BRIN multi minmax date distance',
+  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
+{ oid => '9954', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9955', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9956', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9957', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9958', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9959', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9960', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+{ oid => '9961', descr => 'BRIN multi minmax timestamp distance',
+  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11421,4 +11492,18 @@
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
 
+{ oid => '9962', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9963', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9964', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9965', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 74e279cbf9..e809094490 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -685,4 +685,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+{ oid => '9966',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..e13cb59c7e
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,445 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+ERROR:  value 7 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+DROP INDEX brinidx_multi;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index e568b9fea2..0541c12a25 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9925 | pg_brin_bloom_summary
-(5 rows)
+ 9966 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f1fed1037d..bfbbdbc894 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 9ed1468ad8..54d24cc184 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..6d61fb84c6
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,397 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+
+DROP INDEX brinidx_multi;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0004-BRIN-bloom-indexes-20210211.patchtext/x-patch; charset=UTF-8; name=0004-BRIN-bloom-indexes-20210211.patchDownload
From 4abafeedf5bbafe0617ffa7e8ec6bd3001ccc808 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 4/9] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

The summary does not start as a Bloom filter right away - it initially
stores a plain array of hashes. Only when the size of the array grows
too large it switches to proper Bloom filter. This means that ranges
with low number of distinct values the summary will use less space.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 178 +++++
 doc/src/sgml/ref/create_index.sgml        |  31 +
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 784 ++++++++++++++++++++++
 src/include/access/brin.h                 |   2 +
 src/include/access/brin_internal.h        |   4 +
 src/include/catalog/pg_amop.dat           | 116 ++++
 src/include/catalog/pg_amproc.dat         | 447 ++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 ++
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +-
 src/test/regress/expected/brin_bloom.out  | 428 ++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 376 +++++++++++
 19 files changed, 2531 insertions(+), 6 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index a5271a9f8f..940a0fd8fd 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -581,6 +581,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default value is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default value is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..b54b963f87
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,784 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test whether it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_page.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001		/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25		/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+
+#define BloomMaxFilterSize \
+	MAXALIGN_DOWN(BLCKSZ - \
+				  (MAXALIGN(SizeOfPageHeaderData + \
+							sizeof(ItemIdData)) + \
+				   MAXALIGN(sizeof(BrinSpecialSpace)) + \
+				   SizeOfBrinTuple))
+
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * To calculate the additional hashes (treating the uint32 hash as an input
+ * value), we use the approach with two hash functions from this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions are calculated using hard-coded seeds.
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes
+ * that are not entirely 0. But while indexes don't support TOAST, the
+ * varlena can still be compressed. So this seems unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high. But even if the false positive rate
+ * exceeds the desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16	flags;
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		nbits;	/* size of filter / number of bits */
+	int		nbytes;	/* size of filter / number of bytes */
+
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	nbits = ceil(- (ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	nbytes = ((nbits + 7) / 8);
+	nbits = nbytes * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * Initially the bloom filter is just zeroes and so very compressible,
+	 * but as we add values it gets more and more random, and so less and
+	 * less compressible. So initially everything fits on the page, but
+	 * we might get surprising failures later - we want to prevent that,
+	 * so we reject bloom filter that are obviously too large.
+	 *
+	 * XXX It's not uncommon to oversize the bloom filter a bit, to defend
+	 * against unexpected data anomalies (parts of table with more distinct
+	 * values per range etc.). But we still need to make sure even the
+	 * oversized filter fits on page, if such need arises.
+	 *
+	 * XXX This check is not perfect, because the index may have multiple
+	 * filters that are small individually, but too large when combined.
+	 */
+	if (nbytes > BloomMaxFilterSize)
+		elog(ERROR, "the bloom filter is too large (%d > %zu)", nbytes,
+			 BloomMaxFilterSize);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * nbits / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so
+	 * the varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + nbytes;
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = nbits;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint64	h1, h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint64		h1, h2;
+
+	/* calculate the two hashes */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters for now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int		i;
+	int		nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 79440ebe7b..8cc4e532e6 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..04d678f96a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,20 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2040,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2062,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2084,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2155,14 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2180,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2202,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2224,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2266,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2288,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2439,20 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2470,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2492,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2548,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2570,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2636,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..6709c8dfea 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..dea9adaf98 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9901',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '9902',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '9903',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9904',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9905',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '9906',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '9907',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '9908',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9909',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9910',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9911',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9912',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9913',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '9914',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '9915',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9916',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9917',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9918',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9919',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4099e72001..b7432c65d5 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8214,6 +8214,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '9920', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '9921', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '9922', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '9923', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '9924', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11387,4 +11407,18 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8959c2f53b..74e279cbf9 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,5 +679,10 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-
+{ oid => '9925',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..32c56a996a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,428 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..e568b9fea2 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9925 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 12bb67e491..f1fed1037d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 59b416fd80..9ed1468ad8 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..5d499208e3
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,376 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0003-Optimize-allocations-in-bringetbitmap-20210211.patchtext/x-patch; charset=UTF-8; name=0003-Optimize-allocations-in-bringetbitmap-20210211.patchDownload
From 28a03511b874c8f732c3dc3c4ac834b8c249c7e0 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/9] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 14da9ed17f..c7f7175a2a 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -398,11 +401,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -438,9 +480,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -451,17 +493,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210211.patchtext/x-patch; charset=UTF-8; name=0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210211.patchDownload
From 3cdb5b50eb9cf25b34438dbe935ab437332ee3b7 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/9] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 239 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index dc187153aa..14da9ed17f 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -179,7 +182,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -243,31 +245,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -390,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -416,10 +396,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -440,23 +423,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -464,9 +448,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -544,15 +542,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -695,7 +735,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -724,25 +763,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1521,6 +1543,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1574,3 +1629,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 215bc794d3..f4730be3b9 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -572,37 +504,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 12878ff3a0..6c8852d404 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 78c89a6961..79440ebe7b 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210211.patchtext/x-patch; charset=UTF-8; name=0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210211.patchDownload
From a2923b4d5c659925fe632334fe2268fe90ecd477 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/9] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 27ba596c6e..dc187153aa 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -390,6 +390,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -411,6 +414,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -471,7 +529,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -481,51 +539,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..215bc794d3 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +442,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +462,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +481,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +516,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +526,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..12878ff3a0 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..11835d85cd 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4e0c9be58c..4099e72001 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8191,7 +8191,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8207,7 +8207,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

#146Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#145)
9 attachment(s)
Re: WIP: BRIN multi-range indexes

On 2/11/21 3:51 PM, Tomas Vondra wrote:

...

This passes all my tests, including valgrind on the 32-bit rpi4 machine,
the stress test (testing both the bloom and multi-minmax opclasses etc.)

OK, the cfbot seems happy with it, but I forgot to address the minor
issues mentioned in the review from 2021/02/09, so here's a patch series
addressing that.

Overall, I think the plan is to eventually commit 0001-0004 as is,
squash 0005-0007 (so the minmax-multi uses the "hybrid" approach). I
don't intend to commit 0008, because I have doubts those opclasses are
really useful for anything.

As for 0009, I think it's a fairly small tweak - the correlation made
sense for regular brin indexes, but those new oclasses are meant exactly
for cases where the data is not well correlated.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210215.patchtext/x-patch; charset=UTF-8; name=0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210215.patchDownload
From 06deb48a239a93085e7903c119896bd861480468 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/9] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 150 +++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 170 +++++++++++++++--------
 src/backend/access/brin/brin_minmax.c    | 121 +++++++++++-----
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 324 insertions(+), 125 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 27ba596c6e..dc187153aa 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -390,6 +390,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -411,6 +414,61 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many
+			 * scan keys are there for this attribute, so we simply
+			 * allocate the largest number possible. This may waste
+			 * a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible,
+			 * and it's cheaper than having to repalloc repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -471,7 +529,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -481,51 +539,75 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of multiple
+					 * scan keys. We can determine that based on the number of
+					 * arguments - functions with extra parameter (number of scan
+					 * keys) do support this, otherwise we have to simply pass the
+					 * scan keys one by one,
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same for
+						 * all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet the
+						 * criteria for a single one of them is enough to discard
+						 * the range as a whole, so break out of the loop as soon
+						 * as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..215bc794d3 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -258,53 +260,109 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+				PG_RETURN_BOOL(false);
+			}
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +382,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +442,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +462,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +481,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
+														RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +516,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +526,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..12878ff3a0 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -146,47 +148,104 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		regular_keys = false;
 
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
 		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
 			PG_RETURN_BOOL(false);
 		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
+
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		 /*
+		 * When there are multiple scan keys, failure to meet the
+		 * criteria for a single one of them is enough to discard
+		 * the range as a whole, so break out of the loop as soon
+		 * as a false return value is obtained.
 		 */
-		PG_RETURN_BOOL(false);
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +288,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..11835d85cd 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 1487710d59..33841e14f2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8191,7 +8191,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8207,7 +8207,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210215.patchtext/x-patch; charset=UTF-8; name=0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210215.patchDownload
From 15257fe0278bffebec4049cdaa4d550a4a9b5729 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 17 Sep 2020 17:26:10 +0200
Subject: [PATCH 2/9] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 106 +-------
 src/backend/access/brin/brin_minmax.c    | 103 +-------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 239 insertions(+), 266 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index dc187153aa..14da9ed17f 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+					BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -179,7 +182,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -243,31 +245,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -390,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -416,10 +396,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -440,23 +423,24 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan
+		 * keys are there for a given attribute, so we simply allocate
+		 * the largest number possible (as if all scan keys belonged to
+		 * the same attribute). This may waste a bit of memory, but we
+		 * only expect small number of scan keys in general, so this
+		 * should be negligible, and it's probably cheaper than having
+		 * to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many
-			 * scan keys are there for this attribute, so we simply
-			 * allocate the largest number possible. This may waste
-			 * a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible,
-			 * and it's cheaper than having to repalloc repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -464,9 +448,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -544,15 +542,57 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular
+					 * and IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed.
+					 * If there are no regular scan keys, we're done - the page
+					 * range matches. If there are regular keys, but the page
+					 * range is marked as 'all nulls' it can't possibly pass
+					 * (we're assuming the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -695,7 +735,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -724,25 +763,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1521,6 +1543,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1574,3 +1629,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values;
+	 * our deformed tuple will get updated if the new tuple doesn't fit
+	 * the original range (note this means we can't break out of the loop
+	 * early). Make a note of whether this happens, so that we know to
+	 * insert the modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's
+			 * the first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to
+			 * have only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 215bc794d3..f4730be3b9 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -264,63 +254,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -331,9 +264,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the
@@ -572,37 +504,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 12878ff3a0..6c8852d404 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -152,72 +142,14 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * First check if there are any IS NULL scan keys, and if we're
-	 * violating them. In that case we can terminate early, without
-	 * inspecting the ranges.
-	 */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey	key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* handle IS NULL/IS NOT NULL tests */
-		if (key->sk_flags & SK_ISNULL)
-		{
-			if (key->sk_flags & SK_SEARCHNULL)
-			{
-				if (column->bv_allnulls || column->bv_hasnulls)
-					continue;	/* this key is fine, continue */
-
-				PG_RETURN_BOOL(false);
-			}
-
-			/*
-			 * For IS NOT NULL, we can only skip ranges that are known to have
-			 * only nulls.
-			 */
-			if (key->sk_flags & SK_SEARCHNOTNULL)
-			{
-				if (column->bv_allnulls)
-					PG_RETURN_BOOL(false);
-
-				continue;
-			}
-
-			/*
-			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-			 * operators are strict and return false.
-			 */
-			PG_RETURN_BOOL(false);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* Check that the range is consistent with all scan keys. */
 	for (keyno = 0; keyno < nkeys; keyno++)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		 /*
 		 * When there are multiple scan keys, failure to meet the
@@ -308,34 +240,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 78c89a6961..79440ebe7b 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0003-Optimize-allocations-in-bringetbitmap-20210215.patchtext/x-patch; charset=UTF-8; name=0003-Optimize-allocations-in-bringetbitmap-20210215.patchDownload
From 790d025be147b0e2939517c563f2009cb4a74f88 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Sun, 13 Sep 2020 12:12:47 +0200
Subject: [PATCH 3/9] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 62 +++++++++++++++++++++++++++-------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 14da9ed17f..c7f7175a2a 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -398,11 +401,50 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
 	 * consistent support procedure. We keep null and regular keys separate,
 	 * so that we can easily pass regular keys to the consistent function.
+	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly
+	 * the same lifetime, so that's OK.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		/* regular keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		/* NULL keys */
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -438,9 +480,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -451,17 +493,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0004-BRIN-bloom-indexes-20210215.patchtext/x-patch; charset=UTF-8; name=0004-BRIN-bloom-indexes-20210215.patchDownload
From 4cf2ac64e22aa13b87da3ff863affa58b6a62e4a Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 4/9] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 178 +++++
 doc/src/sgml/ref/create_index.sgml        |  31 +
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 784 ++++++++++++++++++++++
 src/include/access/brin.h                 |   2 +
 src/include/access/brin_internal.h        |   4 +
 src/include/catalog/pg_amop.dat           | 116 ++++
 src/include/catalog/pg_amproc.dat         | 447 ++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 ++
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +-
 src/test/regress/expected/brin_bloom.out  | 428 ++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 376 +++++++++++
 19 files changed, 2531 insertions(+), 6 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index a5271a9f8f..940a0fd8fd 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -581,6 +581,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default value is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default value is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..b54b963f87
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,784 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test whether it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_page.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001		/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25		/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+
+#define BloomMaxFilterSize \
+	MAXALIGN_DOWN(BLCKSZ - \
+				  (MAXALIGN(SizeOfPageHeaderData + \
+							sizeof(ItemIdData)) + \
+				   MAXALIGN(sizeof(BrinSpecialSpace)) + \
+				   SizeOfBrinTuple))
+
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * To calculate the additional hashes (treating the uint32 hash as an input
+ * value), we use the approach with two hash functions from this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions are calculated using hard-coded seeds.
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes
+ * that are not entirely 0. But while indexes don't support TOAST, the
+ * varlena can still be compressed. So this seems unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high. But even if the false positive rate
+ * exceeds the desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16	flags;
+
+	/* fields for the HASHED phase */
+	uint8	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (size) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	int		nbits;	/* size of filter / number of bits */
+	int		nbytes;	/* size of filter / number of bytes */
+
+	double	k;	/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	nbits = ceil(- (ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	nbytes = ((nbits + 7) / 8);
+	nbits = nbytes * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * Initially the bloom filter is just zeroes and so very compressible,
+	 * but as we add values it gets more and more random, and so less and
+	 * less compressible. So initially everything fits on the page, but
+	 * we might get surprising failures later - we want to prevent that,
+	 * so we reject bloom filter that are obviously too large.
+	 *
+	 * XXX It's not uncommon to oversize the bloom filter a bit, to defend
+	 * against unexpected data anomalies (parts of table with more distinct
+	 * values per range etc.). But we still need to make sure even the
+	 * oversized filter fits on page, if such need arises.
+	 *
+	 * XXX This check is not perfect, because the index may have multiple
+	 * filters that are small individually, but too large when combined.
+	 */
+	if (nbytes > BloomMaxFilterSize)
+		elog(ERROR, "the bloom filter is too large (%d > %zu)", nbytes,
+			 BloomMaxFilterSize);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * nbits / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so
+	 * the varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + nbytes;
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = nbits;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint64	h1, h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint64		h1, h2;
+
+	/* calculate the two hashes */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32	h = (h1 + i * h2) % filter->nbits;
+		uint32	byte = (h / 8);
+		uint32	bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double ndistinct;
+	double	maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this
+	 * case to maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a
+	 * couple of safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples,
+	 * in the range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters for now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int		i;
+	int		nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 79440ebe7b..8cc4e532e6 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..04d678f96a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,20 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2040,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2062,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2084,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2155,14 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2180,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2202,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2224,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2266,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2288,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2439,20 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2470,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2492,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2548,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2570,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2636,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..6709c8dfea 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..dea9adaf98 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9901',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '9902',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '9903',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9904',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9905',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '9906',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '9907',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '9908',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9909',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9910',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9911',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9912',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9913',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '9914',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '9915',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9916',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9917',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9918',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9919',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 33841e14f2..3b92bb66e5 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8214,6 +8214,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '9920', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '9921', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '9922', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '9923', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '9924', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11387,4 +11407,18 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8959c2f53b..74e279cbf9 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,5 +679,10 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-
+{ oid => '9925',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..32c56a996a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,428 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..e568b9fea2 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9925 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 12bb67e491..f1fed1037d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 59b416fd80..9ed1468ad8 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..5d499208e3
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,376 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0005-BRIN-minmax-multi-indexes-20210215.patchtext/x-patch; charset=UTF-8; name=0005-BRIN-minmax-multi-indexes-20210215.patchDownload
From f76a9f8f8baa13936c2d0be4ba10fa03a47772b5 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Wed, 3 Feb 2021 19:00:00 +0100
Subject: [PATCH 5/9] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2579 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   18 +
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    8 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 ++++
 src/include/catalog/pg_amproc.dat           |  600 ++++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   85 +
 src/include/catalog/pg_type.dat             |    6 +
 src/test/regress/expected/brin_multi.out    |  445 ++++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  397 +++
 21 files changed, 5041 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 940a0fd8fd..f8a82205d8 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -612,6 +612,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     8 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..2f6e1793dc
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2579 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+#define		MINMAX_LOAD_FACTOR		0.75
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange;		/* number of values per range */
+} MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0))
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, with a simple header
+ * with basic metadata, followed by the boundary values. It has a varlena
+ * header, so can be treated as varlena directly.
+ *
+ * See range_serialize/range_deserialize for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* type of values stored in the data array */
+	Oid		typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+	int		maxvalues;	/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(SerializedRanges *range);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	Ranges *			ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += maxvalues * sizeof(Datum); /* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+	Oid		typid;
+	int		typlen;
+	bool	typbyval;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+	bool	typbyval;
+	int		typlen;
+
+	Ranges *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+
+	range = minmax_multi_init(serialized->maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->maxvalues = serialized->maxvalues;
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * compare_values
+ *	  Compare the values.
+ */
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum *da = (Datum *) a;
+	Datum *db = (Datum *) b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *da, *db);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *db, *da);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+}
+#endif
+
+/*
+ * Comprehensive check of the Ranges structure.
+ */
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i, j;
+
+	/* some basic sanity checks */
+	Assert(ranges->nranges >= 0);
+	Assert(ranges->nvalues >= 0);
+	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
+	Assert(ranges->typid != InvalidOid);
+
+	/*
+	 * First the ranges - there are 2*nranges boundary values, and the
+	 * values have to be strictly ordered (equal values would mean the
+	 * range is collapsed, and should be stored as a point). This also
+	 * guarantees that the ranges do not overlap.
+	 */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundary values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+
+	/* finally check that none of the values are not covered by ranges */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	value = ranges->values[2 * ranges->nranges + i];
+
+		for (j = 0; j < ranges->nranges; j++)
+		{
+			Datum	r;
+
+			Datum	minval = ranges->values[2 * j];
+			Datum	maxval = ranges->values[2 * j + 1];
+
+			/* if value is smaller than range minimum, that's OK */
+			r = FunctionCall2Coll(cmpFn, colloid, value, minval);
+			if (DatumGetBool(r))
+				continue;
+
+			/* if value is greater than range maximum, that's OK */
+			r = FunctionCall2Coll(cmpFn, colloid, maxval, value);
+			if (DatumGetBool(r))
+				continue;
+
+			/* value is between [min,max], which is wrong */
+			Assert(false);
+		}
+	}
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ * This is intentionally sorting the distances in descending order, i.e.
+ * the longer gaps will be at the front.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return 1;
+	else if (da->value > db->value)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (max-min) for pairs
+ * of consecutive ranges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the gap (between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index of the gap the distance is for */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/*
+	 * Sort the distances in descending order, so that the longest gaps
+	 * are at the front.
+	 */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue),
+			 compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+#endif
+
+/*
+ * reduce_combine_ranges
+ *		reduce the ranges until the number of values is low enough
+ *
+ * Combines ranges until the number of boundary values drops below the
+ * threshold specified by max_values. This happens by merging enough
+ * ranges by distance between them.
+ *
+ * Returns the number of result ranges.
+ *
+ * We simply use the global min/max and then add boundaries for enough
+ * largest gaps. Each gap adds 2 values, so we simply use (target/2-1)
+ * distances. Then we simply sort all the values - each two values are
+ * a boundary of a range (possibly collapsed).
+ *
+ * XXX Some of the ranges may be collapsed (i.e. the min/max values are
+ * equal), but we ignore that for now. We could repeat the process,
+ * adding a couple more gaps recursively.
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ *
+ * XXX Or maybe we could just handle this by using random value as a
+ * tie-break, or by adding random noise to the actual distance.
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances, int max_values,
+					  FmgrInfo *cmp, Oid colloid)
+{
+	int i;
+	int		nvalues;
+	Datum  *values;
+
+	compare_context cxt;
+
+	/* total number of gaps between ranges */
+	int	ndistances = (ncranges - 1);
+
+	/* number of gaps to keep */
+	int keep = (max_values/2 - 1);
+
+	/*
+	 * Maybe we have sufficiently low number of ranges already?
+	 *
+	 * XXX This should happen before we actually do the expensive stuff
+	 * like sorting, so maybe this should be just an assert.
+	 */
+	if (keep >= ndistances)
+		return ncranges;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	/* add the global min/max values, from the first/last range */
+	values[nvalues++] = cranges[0].minval;
+	values[nvalues++] = cranges[ncranges-1].maxval;
+
+	/* add boundary values for enough gaps */
+	for (i = 0; i < keep; i++)
+	{
+		/* index of the gap between (index) and (index+1) ranges */
+		int index = distances[i].index;
+
+		Assert((index >= 0) && ((index+1) < ncranges));
+
+		/* add max from the preceding range, minval from the next one */
+		values[nvalues++] = cranges[index].maxval;
+		values[nvalues++] = cranges[index+1].minval;
+
+		Assert(nvalues <= max_values);
+	}
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/*
+	 * Sort the values using the comparator function, and form ranges
+	 * from the sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		cranges[i].minval = values[2*i];
+		cranges[i].maxval = values[2*i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		cranges[i].collapsed = (compare_values(&values[2*i],
+											   &values[2*i+1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+
+	Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues);
+	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
+	{
+		Datum	   *values;
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertCheckRanges(ranges, cmpFn, colloid);
+
+		/* Also check the range contains the value we just added. */
+		// FIXME Assert(ranges, cmpFn, colloid);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 *
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges->maxvalues * MINMAX_LOAD_FACTOR,
+									 cmpFn, colloid);
+
+	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Did we break the ranges somehow? */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+	// FIXME Assert(ranges, cmpFn, colloid);
+
+	return true;
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	/*
+	 * We use the no-check variants here, because user-supplied values
+	 * may have (ip_posid == 0). See ItemPointerCompare.
+	 */
+	da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumberNoCheck(pa1);
+
+	da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumberNoCheck(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	float8		delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute approximate distance between two dates.
+ */
+Datum
+brin_minmax_multi_distance_date(PG_FUNCTION_ARGS)
+{
+	DateADT		dateVal1 = PG_GETARG_DATEADT(0);
+	DateADT		dateVal2 = PG_GETARG_DATEADT(1);
+
+	PG_RETURN_FLOAT8(dateVal1 - dateVal2);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	float8	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	float8	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+Datum
+brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS)
+{
+	float8	delta = 0;
+
+	Timestamp	dt1 = PG_GETARG_TIMESTAMP(0);
+	Timestamp	dt2 = PG_GETARG_TIMESTAMP(1);
+
+	if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
+		PG_RETURN_FLOAT8(0);
+
+	delta = dt2 - dt1;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	float8	delta = 0;
+
+	Interval   *ia = PG_GETARG_INTERVAL_P(0);
+	Interval   *ib = PG_GETARG_INTERVAL_P(1);
+	Interval   *result;
+
+	result = (Interval *) palloc(sizeof(Interval));
+
+	result->month = ib->month - ia->month;
+	/* overflow check copied from int4mi */
+	if (!SAMESIGN(ib->month, ia->month) &&
+		!SAMESIGN(result->month, ib->month))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->day = ib->day - ia->day;
+	if (!SAMESIGN(ib->day, ia->day) &&
+		!SAMESIGN(result->day, ib->day))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->time = ib->time - ia->time;
+	if (!SAMESIGN(ib->time, ia->time) &&
+		!SAMESIGN(result->time, ib->time))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	/*
+	 * We assume months have 31 days - we don't need to be precise, in
+	 * the worst case we'll build somewhat less efficient ranges.
+	 */
+	delta = (float8) (result->month * 31 + result->day);
+
+	/* convert to microseconds (just like the time part) */
+	delta = 24L * 3600L * delta;
+
+	/* and add the time part */
+	delta += result->time / (float8) 1000000.0;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	float8	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	float8	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	float8	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((float8) b->h - (float8) a->h);
+	delta /= 256;
+
+	delta += ((float8) b->g - (float8) a->g);
+	delta /= 256;
+
+	delta += ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8)b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	float8				delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		PG_RETURN_FLOAT8(1.0);
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (float8) addrb[i] - (float8) addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
+{
+	Ranges *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		ranges->typid = attr->atttypid;
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	// MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a);
+	ranges_b = range_deserialize(serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/*
+	 * If needed, reduce some of the ranges.
+	 *
+	 * XXX This may be fairly expensive, so maybe we should do it only when
+	 * it's actually needed (when we have too many ranges).
+	 */
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges,
+	 * and if needed combine as many off them to get below the threshold.
+	 * The collapsed ranges will be stored as a single value.
+	 *
+	 * XXX This does not apply the load factor, as we don't expect to
+	 * add more values to the range, so we prefer to keep as many ranges
+	 * as possible.
+	 *
+	 * XXX Can the maxvalues be different in the two ranges? Perhaps
+	 * we should use maximum of those?
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+									 ranges_a->maxvalues,
+									 cmpFn, colloid);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 8, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum	a, b;
+		text   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum	a;
+		text   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..bf8635d788 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,15 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(
+				brdesc,
+				tuple->bt_columns[keyno].bv_mem_value,
+				tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -495,6 +504,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +588,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 8cc4e532e6..fdaff42722 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..87de94f397 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,11 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+/*
+ * The BRIN opclasses may register serialization callback, in case the on-disk
+ * and in-memory representations differ (e.g. for performance reasons).
+ */
+typedef void (*brin_serialize_callback_type) (BrinDesc *bdesc, Datum src, Datum *dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +32,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 82e874130d..654584a03f 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 04d678f96a..8135854163 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2062,6 +2208,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2088,6 +2251,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2155,6 +2334,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2180,6 +2425,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2202,6 +2464,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2224,6 +2503,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2288,6 +2584,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2439,6 +2752,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2470,6 +2929,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2492,6 +2968,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2548,6 +3041,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2570,6 +3080,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2636,6 +3163,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 6709c8dfea..51403716b1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index dea9adaf98..ba9231ac8c 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9926',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '9901',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9927',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '9902',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9928',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '9904',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9929',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '9905',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9930',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '9909',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '9910',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9931',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9932',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '9911',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9933',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '9912',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9934',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '9913',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9935',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '9914',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9936',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '9916',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9937',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '9917',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9938',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '9918',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9939',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '9919',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3b92bb66e5..728763b97e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8197,6 +8197,77 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9940', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9941', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9942', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9943', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9944', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9945', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9946', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9947', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9948', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9949', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9950', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9951', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9952', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9953', descr => 'BRIN multi minmax date distance',
+  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
+{ oid => '9954', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9955', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9956', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9957', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9958', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9959', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9960', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+{ oid => '9961', descr => 'BRIN multi minmax timestamp distance',
+  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11421,4 +11492,18 @@
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
 
+{ oid => '9962', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9963', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9964', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9965', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 74e279cbf9..e809094490 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -685,4 +685,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+{ oid => '9966',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..e13cb59c7e
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,445 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+ERROR:  value 7 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+DROP INDEX brinidx_multi;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index e568b9fea2..0541c12a25 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9925 | pg_brin_bloom_summary
-(5 rows)
+ 9966 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f1fed1037d..bfbbdbc894 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 9ed1468ad8..54d24cc184 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..6d61fb84c6
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,397 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+
+DROP INDEX brinidx_multi;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0006-Batch-mode-when-building-new-BRIN-multi-min-20210215.patchtext/x-patch; charset=UTF-8; name=0006-Batch-mode-when-building-new-BRIN-multi-min-20210215.patchDownload
From 5a5d2a009e4d0255d55f5da165df6f95762f9e3b Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Tue, 2 Feb 2021 01:49:56 +0100
Subject: [PATCH 6/9] Batch mode when building new BRIN multi-minmax range

Strict enforcement of the values_per_range limit may be quite expensive,
particularly when adding many values into the same range, e.g. when
building a new index. This commit adds a "batch" mode which allows the
buffer to be much larger, so that the compaction happens only once at
the very end, as part of summary serialization. This amortizes the cost
as it's much more efficient to sort many values once.
---
 src/backend/access/brin/brin_minmax_multi.c | 227 ++++++++++++++++++--
 1 file changed, 204 insertions(+), 23 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 2f6e1793dc..69a72da337 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -57,6 +57,7 @@
 #include "access/brin.h"
 #include "access/brin_internal.h"
 #include "access/brin_tuple.h"
+#include "access/hash.h"	/* XXX strange that it fails because of BRIN_AM_OID without this */
 #include "access/reloptions.h"
 #include "access/stratnum.h"
 #include "access/htup_details.h"
@@ -150,13 +151,28 @@ typedef struct MinMaxOptions
  */
 typedef struct Ranges
 {
-	Oid		typid;
+	/* Cache information that we need quite often. */
+	Oid			typid;
+	Oid			colloid;
+	AttrNumber	attno;
 
 	/* (2*nranges + nvalues) <= maxvalues */
 	int		nranges;	/* number of ranges in the array (stored) */
 	int		nvalues;	/* number of values in the data array (all) */
 	int		maxvalues;	/* maximum number of values (reloption) */
 
+	/*
+	 * In batch mode, we simply add the values into a buffer, without any
+	 * expensive steps (sorting, deduplication, ...). The buffer is sized
+	 * to be larger than the target number of values per range, which
+	 * reduces the number of compactions - operating on larger buffers is
+	 * significantly more efficient, in most cases. We keep the actual
+	 * target and compact to the requested number of values at the very
+	 * end, before serializing to on-disk representation.
+	 */
+	bool	batch_mode;
+	int		target_maxvalues;	/* requested number of values */
+
 	/* values stored for this range - either raw values, or ranges */
 	Datum	values[FLEXIBLE_ARRAY_MEMBER];
 } Ranges;
@@ -376,6 +392,7 @@ range_deserialize(SerializedRanges *serialized)
 	range->nvalues = serialized->nvalues;
 	range->maxvalues = serialized->maxvalues;
 	range->typid = serialized->typid;
+	range->batch_mode = false;
 
 	typbyval = get_typbyval(serialized->typid);
 	typlen = get_typlen(serialized->typid);
@@ -753,10 +770,6 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
 	 */
 	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
 
-	/* then the single-point ranges (with nvalues boundary values ) */
-	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
-					 ranges->nvalues);
-
 	/* finally check that none of the values are not covered by ranges */
 	for (i = 0; i < ranges->nvalues; i++)
 	{
@@ -880,12 +893,22 @@ fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
 
 /*
  * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ *
+ * Optionally, the cranges may be deduplicated (this matters in batch mode,
+ * where we simply append values, without checking for duplicates etc.).
+ *
+ * Returns the number of combine ranges.
  */
-static void
+static int
 sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
-					CombineRange *cranges, int ncranges)
+					CombineRange *cranges, int ncranges,
+					bool deduplicate)
 {
-	compare_context cxt;
+	int				n;
+	int				i;
+	compare_context	cxt;
+
+	Assert(ncranges > 0);
 
 	/* sort the values */
 	cxt.colloid = colloid;
@@ -893,6 +916,26 @@ sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
 
 	qsort_arg(cranges, ncranges, sizeof(CombineRange),
 			  compare_combine_ranges, (void *) &cxt);
+
+	if (!deduplicate)
+		return ncranges;
+
+	/* optionally deduplicate the ranges */
+	n = 1;
+	for (i = 1; i < ncranges; i++)
+	{
+		if (compare_combine_ranges(&cranges[i-1], &cranges[i], (void *) &cxt))
+		{
+			if (i != n)
+				memcpy(&cranges[n], &cranges[i], sizeof(CombineRange));
+
+			n++;
+		}
+	}
+
+	Assert((n > 0) && (n <= ncranges));
+
+	return n;
 }
 
 /*
@@ -1063,26 +1106,40 @@ build_distances(FmgrInfo *distanceFn, Oid colloid,
  */
 static CombineRange *
 build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
-					 Datum newvalue, int *nranges)
+					 bool addvalue, Datum newvalue, int *nranges,
+					 bool deduplicate)
 {
 	int				ncranges;
 	CombineRange   *cranges;
 
 	/* now do the actual merge sort */
-	ncranges = ranges->nranges + ranges->nvalues + 1;
+	ncranges = ranges->nranges + ranges->nvalues;
+
+	/* should we add an extra value? */
+	if (addvalue)
+		ncranges += 1;
+
 	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
-	*nranges = ncranges;
 
 	/* put the new value at the beginning */
-	cranges[0].minval = newvalue;
-	cranges[0].maxval = newvalue;
-	cranges[0].collapsed = true;
+	if (addvalue)
+	{
+		cranges[0].minval = newvalue;
+		cranges[0].maxval = newvalue;
+		cranges[0].collapsed = true;
 
-	/* then the regular and collapsed ranges */
-	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+		/* then the regular and collapsed ranges */
+		fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+	}
+	else
+		fill_combine_ranges(cranges, ncranges, ranges);
 
 	/* and sort the ranges */
-	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+	ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges,
+								   deduplicate);
+
+	/* remember how many cranges we built */
+	*nranges = ncranges;
 
 	return cranges;
 }
@@ -1295,6 +1352,27 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	/* comprehensive checks of the input ranges */
 	AssertCheckRanges(ranges, cmpFn, colloid);
 
+	Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0));
+
+	/*
+	 * When batch-building, there should be no ranges. So either the
+	 * number of ranges is 0 or we're not in batching mode.
+	 */
+	Assert(!ranges->batch_mode || (ranges->nranges == 0));
+
+	/* When batch-building, just add it and we're done. */
+	if (ranges->batch_mode)
+	{
+		/* there has to be free space, if we've sized the struct */
+		Assert(ranges->nvalues < ranges->maxvalues);
+
+		/* Make a copy of the value, if needed. */
+		ranges->values[ranges->nvalues++]
+			= datumCopy(newval, attr->attbyval, attr->attlen);;
+
+		return true;
+	}
+
 	/*
 	 * Bail out if the value already is covered by the range.
 	 *
@@ -1375,7 +1453,9 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	oldctx = MemoryContextSwitchTo(ctx);
 
 	/* OK build the combine ranges */
-	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+	cranges = build_combine_ranges(cmpFn, colloid, ranges,
+								   true, newval, &ncranges,
+								   false);
 
 	/* and we'll also need the 'distance' procedure */
 	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
@@ -1407,6 +1487,82 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	return true;
 }
 
+/*
+ * Generate range representation of data collected during "batch mode".
+ * This is similar to reduce_combine_ranges, except that we can't assume
+ * the values are sorted and there may be duplicate values.
+ */
+static void
+compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	/*
+	 * This should only be used in batch mode, and there should be no
+	 * ranges, just individual values.
+	 */
+	Assert((ranges->batch_mode) && (ranges->nranges == 0));
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
+											   BTLessStrategyNumber);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, ranges->attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges,
+								   false, (Datum) 0, &ncranges,
+								   true);	/* deduplicate */
+
+	if (ncranges > 1)
+	{
+		/* build array of gap distances and sort them in ascending order */
+		distances = build_distances(distanceFn, ranges->colloid, cranges, ncranges);
+
+		/*
+		 * Combine ranges until we get below max_values. We don't use any scale
+		 * factor, because this is used at the very end of "batch mode" and we
+		 * don't expect more tuples to be inserted soon.
+		 */
+		ncranges = reduce_combine_ranges(cranges, ncranges, distances,
+										  max_values, cmpFn, ranges->colloid);
+
+		Assert(count_values(cranges, ncranges) <= max_values);
+	}
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	/* check all the range invariants */
+	AssertCheckRanges(ranges, cmpFn, ranges->colloid);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+}
+
 Datum
 brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 {
@@ -1890,7 +2046,16 @@ static void
 brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
 {
 	Ranges *ranges = (Ranges *) DatumGetPointer(src);
-	SerializedRanges *s = range_serialize(ranges);
+	SerializedRanges *s;
+
+	/*
+	 * In batch mode, we need to compress the accumulated values to the
+	 * actually requested number of values/ranges.
+	 */
+	if (ranges->batch_mode)
+		compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+
+	s = range_serialize(ranges);
 	dst[0] = PointerGetDatum(s);
 }
 
@@ -1933,15 +2098,31 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	/*
 	 * If this is the first non-null value, we need to initialize the range
 	 * list. Otherwise just extract the existing range list from BrinValues.
+	 *
+	 * When starting with an empty range, we assume this is a batch mode,
+	 * i.e. we size the buffer for the maximum possible number of items in
+	 * the range (based on range size and max number of items on a page).
+	 *
+	 * XXX This may require quite a bit of memory, so maybe we should use
+	 * some value in between. OTOH most tables will have much wider rows,
+	 * so the number of rows per page is much lower.
+	 *
+	 * XXX Maybe we should do this (using larger buffer) even when there
+	 * already is a summary?
 	 */
 	if (column->bv_allnulls)
 	{
 		MemoryContext oldctx;
 
-		oldctx = MemoryContextSwitchTo(column->bv_context);
+		BlockNumber		pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
 
-		ranges = minmax_multi_init(brin_minmax_multi_get_values(bdesc, opts));
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+		ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange);
+		ranges->attno = attno;
+		ranges->colloid = colloid;
 		ranges->typid = attr->atttypid;
+		ranges->batch_mode = true;
+		ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
 
 		MemoryContextSwitchTo(oldctx);
 
@@ -2227,8 +2408,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 											 BTLessStrategyNumber);
 
-	/* sort the combine ranges */
-	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+	/* sort the combine ranges (don't deduplicate) */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false);
 
 	/*
 	 * We've merged two different lists of ranges, so some of them may be
-- 
2.26.2

0007-Remove-the-special-batch-mode-use-a-larger--20210215.patchtext/x-patch; charset=UTF-8; name=0007-Remove-the-special-batch-mode-use-a-larger--20210215.patchDownload
From bb725fab99a39c0a6c96b3a81bf8a3303ef5d8eb Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Tue, 2 Feb 2021 01:57:54 +0100
Subject: [PATCH 7/9] Remove the special batch mode, use a larger buffer always

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-specified size). That should give
us most of the benefits without consuming a lot of memory.
---
 src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++--------
 1 file changed, 526 insertions(+), 327 deletions(-)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 69a72da337..fd85c18d83 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -57,11 +57,11 @@
 #include "access/brin.h"
 #include "access/brin_internal.h"
 #include "access/brin_tuple.h"
-#include "access/hash.h"	/* XXX strange that it fails because of BRIN_AM_OID without this */
 #include "access/reloptions.h"
 #include "access/stratnum.h"
 #include "access/htup_details.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_am.h"
 #include "catalog/pg_amop.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -92,7 +92,15 @@
  */
 #define		PROCNUM_BASE			11
 
-#define		MINMAX_LOAD_FACTOR		0.75
+/*
+ * Sizing the insert buffer - we use 10x the number of values specified
+ * in the reloption, but we cap it to 8192 not to get too large. When
+ * the buffer gets full, we reduce the number of values by half.
+ */
+#define		MINMAX_BUFFER_FACTOR			10
+#define		MINMAX_BUFFER_MIN				256
+#define		MINMAX_BUFFER_MAX				8192
+#define		MINMAX_BUFFER_LOAD_FACTOR		0.5
 
 typedef struct MinmaxMultiOpaque
 {
@@ -155,23 +163,24 @@ typedef struct Ranges
 	Oid			typid;
 	Oid			colloid;
 	AttrNumber	attno;
+	FmgrInfo   *cmp;
 
 	/* (2*nranges + nvalues) <= maxvalues */
 	int		nranges;	/* number of ranges in the array (stored) */
+	int		nsorted;	/* number of sorted values (ranges + points) */
 	int		nvalues;	/* number of values in the data array (all) */
 	int		maxvalues;	/* maximum number of values (reloption) */
 
 	/*
-	 * In batch mode, we simply add the values into a buffer, without any
-	 * expensive steps (sorting, deduplication, ...). The buffer is sized
-	 * to be larger than the target number of values per range, which
-	 * reduces the number of compactions - operating on larger buffers is
-	 * significantly more efficient, in most cases. We keep the actual
-	 * target and compact to the requested number of values at the very
-	 * end, before serializing to on-disk representation.
+	 * We simply add the values into a large buffer, without any expensive
+	 * steps (sorting, deduplication, ...). The buffer is a multiple of
+	 * the target number of values, so the compaction happen less often,
+	 * amortizing the costs. We keep the actual target and compact to
+	 * the requested number of values at the very end, before serializing
+	 * to on-disk representation.
 	 */
-	bool	batch_mode;
-	int		target_maxvalues;	/* requested number of values */
+	/* requested number of values */
+	int		target_maxvalues;
 
 	/* values stored for this range - either raw values, or ranges */
 	Datum	values[FLEXIBLE_ARRAY_MEMBER];
@@ -203,7 +212,7 @@ typedef struct SerializedRanges
 
 static SerializedRanges *range_serialize(Ranges *range);
 
-static Ranges *range_deserialize(SerializedRanges *range);
+static Ranges *range_deserialize(int maxvalues, SerializedRanges *range);
 
 /* Cache for support and strategy procesures. */
 
@@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
 static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
 					   uint16 attno, Oid subtype, uint16 strategynum);
 
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+static int compare_values(const void *a, const void *b, void *arg);
+
 
 /*
  * minmax_multi_init
@@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues)
 	return ranges;
 }
 
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid);
+
+
+static void
+range_deduplicate_values(Ranges *range)
+{
+	int				i, n;
+	int				start;
+	compare_context cxt;
+
+	/*
+	 * If there are no unsorted values, we're done (this probably can't
+	 * happen, as we're adding values to unsorted part).
+	 */
+	if (range->nsorted == range->nvalues)
+		return;
+
+	/* sort the values */
+	cxt.colloid = range->colloid;
+	cxt.cmpFn = range->cmp;
+
+	/* how many values to sort? */
+	start = 2 * range->nranges;
+
+	qsort_arg(&range->values[start],
+			  range->nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	n = 1;
+	for (i = 1; i < range->nvalues; i++)
+	{
+		/* same as preceding value, so store it */
+		if (compare_values(&range->values[start + i - 1],
+						   &range->values[start + i],
+						   (void *) &cxt) == 0)
+			continue;
+
+		range->values[start + n] = range->values[start + i];
+
+		n++;
+	}
+
+	/* now all the values are sorted */
+	range->nvalues = n;
+	range->nsorted = n;
+
+	AssertCheckRanges(range, range->cmp, range->colloid);
+}
+
+
 /*
  * range_serialize
  *	  Serialize the in-memory representation into a compact varlena value.
@@ -262,14 +330,25 @@ range_serialize(Ranges *range)
 
 	/* simple sanity checks */
 	Assert(range->nranges >= 0);
+	Assert(range->nsorted >= 0);
 	Assert(range->nvalues >= 0);
 	Assert(range->maxvalues > 0);
+	Assert(range->target_maxvalues > 0);
+
+	/* at this point the range should be compacted to the target size */
+	Assert(2*range->nranges + range->nvalues <= range->target_maxvalues);
+
+	Assert(range->target_maxvalues <= range->maxvalues);
+
+	/* range boundaries are always sorted */
+	Assert(range->nvalues >= range->nsorted);
+
+	/* sort and deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
 
 	/* see how many Datum values we actually have */
 	nvalues = 2*range->nranges + range->nvalues;
 
-	Assert(2*range->nranges + range->nvalues <= range->maxvalues);
-
 	typid = range->typid;
 	typbyval = get_typbyval(typid);
 	typlen = get_typlen(typid);
@@ -316,7 +395,7 @@ range_serialize(Ranges *range)
 	serialized->typid = typid;
 	serialized->nranges = range->nranges;
 	serialized->nvalues = range->nvalues;
-	serialized->maxvalues = range->maxvalues;
+	serialized->maxvalues = range->target_maxvalues;
 
 	/*
 	 * And now copy also the boundary values (like the length calculation
@@ -367,7 +446,7 @@ range_serialize(Ranges *range)
  * in the in-memory value array.
  */
 static Ranges *
-range_deserialize(SerializedRanges *serialized)
+range_deserialize(int maxvalues, SerializedRanges *serialized)
 {
 	int		i,
 			nvalues;
@@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized)
 	nvalues = 2*serialized->nranges + serialized->nvalues;
 
 	Assert(nvalues <= serialized->maxvalues);
+	Assert(serialized->maxvalues <= maxvalues);
 
-	range = minmax_multi_init(serialized->maxvalues);
+	range = minmax_multi_init(maxvalues);
 
 	/* copy the header info */
 	range->nranges = serialized->nranges;
 	range->nvalues = serialized->nvalues;
-	range->maxvalues = serialized->maxvalues;
+	range->nsorted = serialized->nvalues;
+	range->maxvalues = maxvalues;
+	range->target_maxvalues = serialized->maxvalues;
+
 	range->typid = serialized->typid;
-	range->batch_mode = false;
 
 	typbyval = get_typbyval(serialized->typid);
 	typlen = get_typlen(serialized->typid);
@@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized)
 	return range;
 }
 
-typedef struct compare_context
-{
-	FmgrInfo   *cmpFn;
-	Oid			colloid;
-} compare_context;
-
 /*
  * Used to represent ranges expanded during merging and combining (to
  * reduce number of boundary values to store).
@@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg)
 	return 0;
 }
 
+void *bsearch_arg(const void *key, const void *base,
+						 size_t nmemb, size_t size,
+						 int (*compar) (const void *, const void *, void *),
+						 void *arg);
+
+static bool
+has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges,
+				   Datum newval, AttrNumber attno, Oid typid)
+{
+	Datum	compar;
+
+	Datum	minvalue = ranges->values[0];
+	Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+	FmgrInfo *cmpLessFn;
+	FmgrInfo *cmpGreaterFn;
+
+	/* binary search on ranges */
+	int		start,
+			end;
+
+	if (ranges->nranges == 0)
+		return false;
+
+	/*
+	 * Otherwise, need to compare the new value with boundaries of all
+	 * the ranges. First check if it's less than the absolute minimum,
+	 * which is the first value in the array.
+	 */
+	cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+										 BTLessStrategyNumber);
+	compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+	/* smaller than the smallest value in the range list */
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * And now compare it to the existing maximum (last value in the
+	 * data array). But only if we haven't already ruled out a possible
+	 * match in the minvalue check.
+	 */
+	cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+										BTGreaterStrategyNumber);
+	compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * So we know it's in the general min/max, the question is whether it
+	 * falls in one of the ranges or gaps. We'll use a binary search on
+	 * the ranges.
+	 *
+	 * it's in the general range, but is it actually covered by any
+	 * of the ranges? Repeat the check for each range.
+	 *
+	 * XXX We simply walk the ranges sequentially, but maybe we could
+	 * further leverage the ordering and non-overlap and use bsearch to
+	 * speed this up a bit.
+	 */
+	start = 0;					/* first range */
+	end = ranges->nranges - 1;	/* last range */
+	while (true)
+	{
+		int		midpoint = (start + end) / 2;
+
+		/* this means we ran out of ranges in the last step */
+		if (start > end)
+			return false;
+
+		/* copy the min/max values from the ranges */
+		minvalue = ranges->values[2 * midpoint];
+		maxvalue = ranges->values[2 * midpoint + 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse
+		 * to the left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in this range */
+		if (DatumGetBool(compar))
+		{
+			end = (midpoint - 1);
+			continue;
+		}
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse
+		 * to the right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+		/* larger than the largest value in this range */
+		if (DatumGetBool(compar))
+		{
+			start = (midpoint + 1);
+			continue;
+		}
+
+		/* hey, we found a matching range */
+		return true;
+	}
+
+	return false;
+}
+
+
 /*
  * range_contains_value
  * 		See if the new value is already contained in the range list.
@@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid,
 							Ranges *ranges, Datum newval)
 {
 	int			i;
-	FmgrInfo   *cmpLessFn;
-	FmgrInfo   *cmpGreaterFn;
 	FmgrInfo   *cmpEqualFn;
 	Oid			typid = attr->atttypid;
 
@@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid,
 	 * range, and only when there's still a chance of getting a match we
 	 * inspect the individual ranges.
 	 */
-	if (ranges->nranges > 0)
-	{
-		Datum	compar;
-		bool	match = true;
-
-		Datum	minvalue = ranges->values[0];
-		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
-
-		/*
-		 * Otherwise, need to compare the new value with boundaries of all
-		 * the ranges. First check if it's less than the absolute minimum,
-		 * which is the first value in the array.
-		 */
-		cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
-											 BTLessStrategyNumber);
-		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
-
-		/* smaller than the smallest value in the range list */
-		if (DatumGetBool(compar))
-			match = false;
-
-		/*
-		 * And now compare it to the existing maximum (last value in the
-		 * data array). But only if we haven't already ruled out a possible
-		 * match in the minvalue check.
-		 */
-		if (match)
-		{
-			cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
-												BTGreaterStrategyNumber);
-			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
-
-			if (DatumGetBool(compar))
-				match = false;
-		}
-
-		/*
-		 * So it's in the general range, but is it actually covered by any
-		 * of the ranges? Repeat the check for each range.
-		 *
-		 * XXX We simply walk the ranges sequentially, but maybe we could
-		 * further leverage the ordering and non-overlap and use bsearch to
-		 * speed this up a bit.
-		 */
-		for (i = 0; i < ranges->nranges && match; i++)
-		{
-			/* copy the min/max values from the ranges */
-			minvalue = ranges->values[2*i];
-			maxvalue = ranges->values[2*i+1];
-
-			/*
-			 * Otherwise, need to compare the new value with boundaries of all
-			 * the ranges. First check if it's less than the absolute minimum,
-			 * which is the first value in the array.
-			 */
-			compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
-
-			/* smaller than the smallest value in this range */
-			if (DatumGetBool(compar))
-				continue;
-
-			compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
-
-			/* larger than the largest value in this range */
-			if (DatumGetBool(compar))
-				continue;
-
-			/* hey, we found a matching row */
-			return true;
-		}
-	}
+	if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid))
+		return true;
 
 	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
 											 BTEqualStrategyNumber);
@@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid,
 	/*
 	 * We're done with the ranges, now let's inspect the exact values.
 	 *
-	 * XXX Again, we do sequentially search the values - consider leveraging
-	 * the ordering of values to improve performance.
+	 * XXX We do sequential search for small number of values, and bsearch
+	 * once we have more than 16 values.
+	 *
+	 * XXX We only inspect the sorted part - that's OK. For building it may
+	 * produce false negatives, but only after we already added some values
+	 * to the unsorted part, so we've modified the value. And when querying
+	 * the index, there should be no unsorted values.
 	 */
-	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	if (ranges->nsorted >= 16)
 	{
-		Datum compar;
+		compare_context	cxt;
 
-		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
 
-		/* found an exact match */
-		if (DatumGetBool(compar))
+		if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) != NULL)
 			return true;
 	}
-
-	/* the value is not covered by this BRIN tuple */
-	return false;
-}
-
-/*
- * insert_value
- *	  Adds a new value into the single-point part, while maintaining ordering.
- *
- * The function inserts the new value to the right place in the single-point
- * part of the range. It assumes there's enough free space, and then does
- * essentially an insert-sort.
- *
- * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
- * only the first nvalues are used.
- */
-static void
-insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
-			 Datum newvalue)
-{
-	int	i;
-	Datum	lt;
-
-	/* If there are no values yet, store the new one and we're done. */
-	if (!nvalues)
+	else
 	{
-		values[0] = newvalue;
-		return;
-	}
-
-	/*
-	 * A common case is that the new value is entirely out of the existing
-	 * range, i.e. it's either smaller or larger than all previous values.
-	 * So we check and handle this case first - first we check the larger
-	 * case, because in that case we can just append the value to the end
-	 * of the array and we're done.
-	 */
+		for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++)
+		{
+			Datum compar;
 
-	/* Is it greater than all existing values in the array? */
-	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
-	if (DatumGetBool(lt))
-	{
-		/* just copy it in-place and we're done */
-		values[nvalues] = newvalue;
-		return;
-	}
+			compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
 
-	/*
-	 * OK, I lied a bit - we won't check the smaller case explicitly, but
-	 * we'll just compare the value to all existing values in the array.
-	 * But we happen to start with the smallest value, so we're actually
-	 * doing the check anyway.
-	 *
-	 * XXX We do walk the values sequentially. Perhaps we could/should be
-	 * smarter and do some sort of bisection, to improve performance?
-	 */
-	for (i = 0; i < nvalues; i++)
-	{
-		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
-		if (DatumGetBool(lt))
-		{
-			/*
-			 * Move values to make space for the new entry, which should go
-			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
-			 */
-			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
-			values[i] = newvalue;
-			return;
+			/* found an exact match */
+			if (DatumGetBool(compar))
+				return true;
 		}
 	}
 
-	/* We should never really get here. */
-	Assert(false);
+	/* the value is not covered by this BRIN tuple */
+	return false;
 }
 
 #ifdef USE_ASSERT_CHECKING
@@ -754,11 +818,12 @@ static void
 AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
 {
 #ifdef USE_ASSERT_CHECKING
-	int i, j;
+	int i;
 
 	/* some basic sanity checks */
 	Assert(ranges->nranges >= 0);
-	Assert(ranges->nvalues >= 0);
+	Assert(ranges->nsorted >= 0);
+	Assert(ranges->nvalues >= ranges->nsorted);
 	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
 	Assert(ranges->typid != InvalidOid);
 
@@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
 	 */
 	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
 
-	/* finally check that none of the values are not covered by ranges */
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nsorted);
+
+	/*
+	 * Check that none of the values are not covered by ranges (both
+	 * sorted and unsorted)
+	 */
 	for (i = 0; i < ranges->nvalues; i++)
 	{
+		Datum	compar;
+		int		start,
+				end;
+		Datum	minvalue,
+				maxvalue;
+
 		Datum	value = ranges->values[2 * ranges->nranges + i];
 
-		for (j = 0; j < ranges->nranges; j++)
+		if (ranges->nranges == 0)
+			break;
+
+		minvalue = ranges->values[0];
+		maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse
+		 * to the left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+		/* smaller than the smallest value in the first range */
+		if (DatumGetBool(compar))
+			continue;
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse
+		 * to the right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+		/* larger than the largest value in the last range */
+		if (DatumGetBool(compar))
+			continue;
+
+		start = 0;					/* first range */
+		end = ranges->nranges - 1;	/* last range */
+		while (true)
 		{
-			Datum	r;
+			int		midpoint = (start + end) / 2;
+
+			/* this means we ran out of ranges in the last step */
+			if (start > end)
+				break;
+
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2 * midpoint];
+			maxvalue = ranges->values[2 * midpoint + 1];
 
-			Datum	minval = ranges->values[2 * j];
-			Datum	maxval = ranges->values[2 * j + 1];
+			/*
+			 * Is the value smaller than the minval? If yes, we'll recurse
+			 * to the left side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
 
-			/* if value is smaller than range minimum, that's OK */
-			r = FunctionCall2Coll(cmpFn, colloid, value, minval);
-			if (DatumGetBool(r))
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+			{
+				end = (midpoint - 1);
 				continue;
+			}
+
+			/*
+			 * Is the value greater than the minval? If yes, we'll recurse
+			 * to the right side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
 
-			/* if value is greater than range maximum, that's OK */
-			r = FunctionCall2Coll(cmpFn, colloid, maxval, value);
-			if (DatumGetBool(r))
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+			{
+				start = (midpoint + 1);
 				continue;
+			}
 
-			/* value is between [min,max], which is wrong */
+			/* hey, we found a matching range */
 			Assert(false);
 		}
 	}
+
+	/* and values in the unsorted part must not be in sorted part */
+	for (i = ranges->nsorted; i < ranges->nvalues; i++)
+	{
+		compare_context	cxt;
+		Datum	value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nsorted == 0)
+			break;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) == NULL);
+	}
 #endif
 }
 
@@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid,
  */
 static CombineRange *
 build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
-					 bool addvalue, Datum newvalue, int *nranges,
-					 bool deduplicate)
+					 int *nranges)
 {
 	int				ncranges;
 	CombineRange   *cranges;
@@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
 	/* now do the actual merge sort */
 	ncranges = ranges->nranges + ranges->nvalues;
 
-	/* should we add an extra value? */
-	if (addvalue)
-		ncranges += 1;
-
 	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
 
-	/* put the new value at the beginning */
-	if (addvalue)
-	{
-		cranges[0].minval = newvalue;
-		cranges[0].maxval = newvalue;
-		cranges[0].collapsed = true;
-
-		/* then the regular and collapsed ranges */
-		fill_combine_ranges(&cranges[1], ncranges-1, ranges);
-	}
-	else
-		fill_combine_ranges(cranges, ncranges, ranges);
+	/* fll the combine ranges */
+	fill_combine_ranges(cranges, ncranges, ranges);
 
 	/* and sort the ranges */
-	ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges,
-								   deduplicate);
+	ncranges = sort_combine_ranges(cmp, colloid,
+								   cranges, ncranges,
+								   true);	/* deduplicate */
 
 	/* remember how many cranges we built */
 	*nranges = ncranges;
@@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
 		}
 	}
 
+	/* all the values are sorted */
+	ranges->nsorted = ranges->nvalues;
+
 	Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues);
 	Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues);
 }
 
+
+
 /*
- * range_add_value
- * 		Add the new value to the multi-minmax range.
+ * Consider freeing space in the ranges.
+ *
+ * Returns true if the value was actually modified.
  */
 static bool
-range_add_value(BrinDesc *bdesc, Oid colloid,
-				AttrNumber attno, Form_pg_attribute attr,
-				Ranges *ranges, Datum newval)
+ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *range)
 {
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
 	FmgrInfo   *cmpFn,
 			   *distanceFn;
 
@@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	int				ncranges;
 	DistanceValue  *distances;
 
-	MemoryContext	ctx;
-	MemoryContext	oldctx;
-
-	/* we'll certainly need the comparator, so just look it up now */
-	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
-											   BTLessStrategyNumber);
-
-	/* comprehensive checks of the input ranges */
-	AssertCheckRanges(ranges, cmpFn, colloid);
-
-	Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0));
-
 	/*
-	 * When batch-building, there should be no ranges. So either the
-	 * number of ranges is 0 or we're not in batching mode.
+	 * If there is free space in the buffer, we're done without having
+	 * to modify anything.
 	 */
-	Assert(!ranges->batch_mode || (ranges->nranges == 0));
-
-	/* When batch-building, just add it and we're done. */
-	if (ranges->batch_mode)
-	{
-		/* there has to be free space, if we've sized the struct */
-		Assert(ranges->nvalues < ranges->maxvalues);
-
-		/* Make a copy of the value, if needed. */
-		ranges->values[ranges->nvalues++]
-			= datumCopy(newval, attr->attbyval, attr->attlen);;
-
-		return true;
-	}
-
-	/*
-	 * Bail out if the value already is covered by the range.
-	 *
-	 * We could also add values until we hit values_per_range, and then
-	 * do the deduplication in a batch, hoping for better efficiency. But
-	 * that would mean we actually modify the range every time, which means
-	 * having to serialize the value, which does palloc, walks the values,
-	 * copies them, etc. Not exactly cheap.
-	 *
-	 * So instead we do the check, which should be fairly cheap - assuming
-	 * the comparator function is not very expensive.
-	 *
-	 * This also implies means the values array can't contain duplicities.
-	 */
-	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+	if (2*range->nranges + range->nvalues < range->maxvalues)
 		return false;
 
-	/* Make a copy of the value, if needed. */
-	newval = datumCopy(newval, attr->attbyval, attr->attlen);
-
-	/*
-	 * If there's space in the values array, copy it in and we're done.
-	 *
-	 * We do want to keep the values sorted (to speed up searches), so we
-	 * do a simple insertion sort. We could do something more elaborate,
-	 * e.g. by sorting the values only now and then, but for small counts
-	 * (e.g. when maxvalues is 64) this should be fine.
-	 */
-	if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues)
-	{
-		Datum	   *values;
-
-		/* beginning of the 'single value' part (for convenience) */
-		values = &ranges->values[2*ranges->nranges];
-
-		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
-
-		ranges->nvalues++;
-
-		/*
-		 * Check we haven't broken the ordering of boundary values (checks
-		 * both parts, but that doesn't hurt).
-		 */
-		AssertCheckRanges(ranges, cmpFn, colloid);
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
 
-		/* Also check the range contains the value we just added. */
-		// FIXME Assert(ranges, cmpFn, colloid);
+	/* Try deduplicating values in the unsorted part */
+	range_deduplicate_values(range);
 
-		/* yep, we've modified the range */
+	/* did we reduce enough free space by just the deduplication? */
+	if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR)
 		return true;
-	}
 
 	/*
-	 * Damn - the new value is not in the range yet, but we don't have space
-	 * to just insert it. So we need to combine some of the existing ranges,
-	 * to reduce the number of values we need to store (joining two intervals
-	 * reduces the number of boundaries to store by 2).
+	 * we need to combine some of the existing ranges, to reduce the number
+	 * of values we need to store (joining intervals reduces the number of
+	 * boundary values).
 	 *
-	 * To do that we first construct an array of CombineRange items - each
-	 * combine range tracks if it's a regular range or collapsed range, where
-	 * "collapsed" means "single point."
+	 * We first construct an array of CombineRange items - each combine range
+	 * tracks if it's a regular range or a collapsed range, where "collapsed"
+	 * means "single point." This makes the processing easier, as it allows
+	 * handling ranges and points the same way.
 	 *
-	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
-	 * directly, while single points (ranges->nvalues of them) have to be
-	 * expanded. We neet the combine ranges to be sorted, and we do that by
-	 * performing a merge sort of ranges, values and new value.
+	 * Then we sort the combine ranges - this is necessary, because although
+	 * ranges and points were sorted on their own, the new array is not. We
+	 * do that by performing a merge sort of the two parts.
 	 *
 	 * The distanceFn calls (which may internally call e.g. numeric_le) may
-	 * allocate quite a bit of memory, and we must not leak it. Otherwise
-	 * we'd have problems e.g. when building indexes. So we create a local
-	 * memory context and make sure we free the memory before leaving this
-	 * function (not after every call).
+	 * allocate quite a bit of memory, and we must not leak it (we might have
+	 * to do this repeatedly, even for a single BRIN page range). Otherwise
+	 * we'd have problems e.g. when building new indexes. So we use a memory
+	 * context and make sure we free the memory at the end (so if we call
+	 * the distance function many times, it might be an issue, but meh).
 	 */
 	ctx = AllocSetContextCreate(CurrentMemoryContext,
 								"minmax-multi context",
@@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	oldctx = MemoryContextSwitchTo(ctx);
 
 	/* OK build the combine ranges */
-	cranges = build_combine_ranges(cmpFn, colloid, ranges,
-								   true, newval, &ncranges,
-								   false);
+	cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges);
 
 	/* and we'll also need the 'distance' procedure */
 	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
@@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid,
 	 * use too low or high value.
 	 */
 	ncranges = reduce_combine_ranges(cranges, ncranges, distances,
-									 ranges->maxvalues * MINMAX_LOAD_FACTOR,
+									 range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR,
 									 cmpFn, colloid);
 
-	Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR);
+	Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR);
 
 	/* decompose the combine ranges into regular ranges and single values */
-	store_combine_ranges(ranges, cranges, ncranges);
+	store_combine_ranges(range, cranges, ncranges);
 
 	MemoryContextSwitchTo(oldctx);
 	MemoryContextDelete(ctx);
 
 	/* Did we break the ranges somehow? */
+	AssertCheckRanges(range, cmpFn, colloid);
+
+	return true;
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn;
+	bool		modified = false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
 	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Make sure there's enough free space in the buffer. We only trigger
+	 * this when the buffer is full, which means it had to be modified as
+	 * we size it to be larger than what is stored on disk.
+	 *
+	 * XXX This needs to happen before we check if the value is contained
+	 * in the range, because the value might be in the unsorted part, and
+	 * we don't check that in range_contains_value. The deduplication would
+	 * then move it to the sorted part, and we'd add the value too, which
+	 * violates the rule that we never have duplicates with the ranges
+	 * or sorted values.
+	 *
+	 * XXX At the moment this only does the deduplication.
+	 *
+	 * XXX We might also deduplicate and recheck if the value is contained,
+	 * but that seems like an overkill. We'd need to deduplicate anyway,
+	 * so why not do it now.
+	 */
+	modified = ensure_free_space_in_buffer(bdesc, colloid,
+										   attno, attr, ranges);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return modified;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when maxvalues is 64) this should be fine.
+	 */
+	ranges->values[2*ranges->nranges + ranges->nvalues] = newval;
+	ranges->nvalues++;
+
+	/*
+	 * Check we haven't broken the ordering of boundary values (checks
+	 * both parts, but that doesn't hurt).
+	 */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/* Also check the range contains the value we just added. */
 	// FIXME Assert(ranges, cmpFn, colloid);
 
+	/* yep, we've modified the range */
 	return true;
 }
 
@@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
 	MemoryContext	ctx;
 	MemoryContext	oldctx;
 
-	/*
-	 * This should only be used in batch mode, and there should be no
-	 * ranges, just individual values.
-	 */
-	Assert((ranges->batch_mode) && (ranges->nranges == 0));
-
 	/* we'll certainly need the comparator, so just look it up now */
 	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
 											   BTLessStrategyNumber);
@@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
 
 	/* OK build the combine ranges */
 	cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges,
-								   false, (Datum) 0, &ncranges,
-								   true);	/* deduplicate */
+								   &ncranges);	/* deduplicate */
 
 	if (ncranges > 1)
 	{
@@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
 		 * don't expect more tuples to be inserted soon.
 		 */
 		ncranges = reduce_combine_ranges(cranges, ncranges, distances,
-										  max_values, cmpFn, ranges->colloid);
+										 max_values, cmpFn, ranges->colloid);
 
 		Assert(count_values(cranges, ncranges) <= max_values);
 	}
@@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
 	 * In batch mode, we need to compress the accumulated values to the
 	 * actually requested number of values/ranges.
 	 */
-	if (ranges->batch_mode)
-		compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+	compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
 
 	s = range_serialize(ranges);
 	dst[0] = PointerGetDatum(s);
@@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	{
 		MemoryContext oldctx;
 
+		int				target_maxvalues;
+		int				maxvalues;
 		BlockNumber		pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
 
+		/* what was specified as a reloption? */
+		target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped
+		 * to the maximum number of values in the heap range. This is more
+		 * than enough, considering the actual number of rows per page is
+		 * likely much lower, but meh.
+		 */
+		maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, target_maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
 		oldctx = MemoryContextSwitchTo(column->bv_context);
-		ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange);
+		ranges = minmax_multi_init(maxvalues);
 		ranges->attno = attno;
 		ranges->colloid = colloid;
 		ranges->typid = attr->atttypid;
-		ranges->batch_mode = true;
-		ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+		ranges->target_maxvalues = target_maxvalues;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
 
 		MemoryContextSwitchTo(oldctx);
 
@@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	{
 		MemoryContext oldctx;
 
+		int				maxvalues;
+		BlockNumber		pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
 		oldctx = MemoryContextSwitchTo(column->bv_context);
 
 		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-		ranges = range_deserialize(serialized);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped
+		 * to the maximum number of values in the heap range. This is more
+		 * than enough, considering the actual number of rows per page is
+		 * likely much lower, but meh.
+		 */
+		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, serialized->maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		ranges = range_deserialize(maxvalues, serialized);
+
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
 
 		column->bv_mem_value = PointerGetDatum(ranges);
 		column->bv_serialize = brin_minmax_multi_serialize;
@@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
 	attno = column->bv_attno;
 
 	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
-	ranges = range_deserialize(serialized);
+	ranges = range_deserialize(serialized->maxvalues, serialized);
 
 	/* inspect the ranges, and for each one evaluate the scan keys */
 	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
@@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
 	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
 
-	ranges_a = range_deserialize(serialized_a);
-	ranges_b = range_deserialize(serialized_b);
+	ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a);
+	ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b);
 
 	/* make sure neither of the ranges is NULL */
 	Assert(ranges_a && ranges_b);
@@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS)
 	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 											 BTLessStrategyNumber);
 
-	/* sort the combine ranges (don't deduplicate) */
+	/* sort the combine ranges (no need to deduplicate) */
 	sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false);
 
 	/*
@@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
 	fmgr_info(outfunc, &fmgrinfo);
 
 	/* deserialize the range info easy-to-process pieces */
-	ranges_deserialized = range_deserialize(ranges);
+	ranges_deserialized = range_deserialize(ranges->maxvalues, ranges);
 
 	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
 					 ranges_deserialized->nranges,
-- 
2.26.2

0008-Define-multi-minmax-oclasses-for-types-with-20210215.patchtext/x-patch; charset=UTF-8; name=0008-Define-multi-minmax-oclasses-for-types-with-20210215.patchDownload
From b34ecce5ae969a3d55b6fde541d71f10d84c43c1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Wed, 3 Feb 2021 19:12:05 +0100
Subject: [PATCH 8/9] Define multi-minmax oclasses for types without distance

The existing multi-minmax opclasses rely on "distance" function when
deciding how to build the ranges, covering all the values. In principle,
those opclasses try to minimize the lengths of ranges, i.e. maximize the
lengths of "gaps" between them.

For some data types it's hard to construct a distance function - types
like "text" or "name" generally serve as labels, and may have ordering
only. So this uses a different approach, based on the observation that
it's the outliers that "break" BRIN minmax indexes, i.e. the lowest and
highest values. So simply sort the values, keep the (K-2) extreme values
on either tail, and build a single range representing the values in the
middle. Of course, the question is whether this is actually useful, but
that's hard to judge, and it's the best we can do.
---
 src/backend/access/brin/brin_minmax_multi.c |  75 ++++++++++++
 src/include/catalog/pg_amop.dat             | 119 ++++++++++++++++++++
 src/include/catalog/pg_amproc.dat           | 113 +++++++++++++++++++
 src/include/catalog/pg_opclass.dat          |  21 ++++
 src/include/catalog/pg_opfamily.dat         |  14 +++
 5 files changed, 342 insertions(+)

diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index fd85c18d83..c77c207e94 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1210,6 +1210,9 @@ build_distances(FmgrInfo *distanceFn, Oid colloid,
 
 	Assert(ncranges >= 2);
 
+	if (!distanceFn)
+		return NULL;
+
 	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
 
 	/*
@@ -1298,6 +1301,70 @@ count_values(CombineRange *cranges, int ncranges)
 }
 #endif
 
+
+static int
+reduce_combine_ranges_simple(CombineRange *cranges, int ncranges,
+							 int max_values, FmgrInfo *cmp, Oid colloid)
+{
+	int		i;
+	int		nvalues;
+	Datum  *values;
+
+	compare_context cxt;
+
+	/* number of values to keep on each tail */
+	int tail = (max_values - 2) / 2;
+	int m = Min(tail / 2, ncranges - 1 / 2);
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	for (i = 0; i < m; i++)
+	{
+		/* head */
+		values[nvalues++] = cranges[i].minval;
+		values[nvalues++] = cranges[i].maxval;
+
+		/* tail */
+		values[nvalues++] = cranges[ncranges - 1 - i].minval;
+		values[nvalues++] = cranges[ncranges - 1 - i].maxval;
+	}
+
+	/* middle part */
+	values[nvalues++] = cranges[m].maxval;
+	values[nvalues++] = cranges[ncranges - 1 - m].minval;
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/*
+	 * Sort the values using the comparator function, and form ranges
+	 * from the sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		cranges[i].minval = values[2*i];
+		cranges[i].maxval = values[2*i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		cranges[i].collapsed = (compare_values(&values[2*i],
+											   &values[2*i+1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+
 /*
  * reduce_combine_ranges
  *		reduce the ranges until the number of values is low enough
@@ -1366,6 +1433,14 @@ reduce_combine_ranges(CombineRange *cranges, int ncranges,
 	if (keep >= ndistances)
 		return ncranges;
 
+	/*
+	 * Without distances, we use a simple approach keeping as many
+	 * outliers as possible.
+	 */
+	if (!distances)
+		return reduce_combine_ranges_simple(cranges, ncranges, max_values,
+											cmp, colloid);
+
 	/* sort the values */
 	cxt.colloid = colloid;
 	cxt.cmpFn = cmp;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 8135854163..5cf820f04b 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,23 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# minmax multi bytea
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '<(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '2', amopopr => '<=(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '3', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '4', amopopr => '>=(bytea,bytea)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bytea_minmax_multi_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # bloom bytea
 { amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
@@ -1836,6 +1853,23 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# minmax multi "char"
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '2', amopopr => '<=(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '3', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '4', amopopr => '>=(char,char)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/char_minmax_multi_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
+  amopmethod => 'brin' },
+
 # bloom "char"
 { amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
@@ -1858,6 +1892,23 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# minmax multi name
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '2', amopopr => '<=(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '3', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '4', amopopr => '>=(name,name)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/name_minmax_multi_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
+  amopmethod => 'brin' },
+
 # bloom name
 { amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
@@ -2186,6 +2237,23 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# minmax multi text
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '2', amopopr => '<=(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '3', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '4', amopopr => '>=(text,text)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/text_minmax_multi_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
+  amopmethod => 'brin' },
+
 # bloom text
 { amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
@@ -2562,6 +2630,23 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# minmax multi character
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '<(bpchar,bpchar)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '2',
+  amopopr => '<=(bpchar,bpchar)', amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '3', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '4',
+  amopopr => '>=(bpchar,bpchar)', amopmethod => 'brin' },
+{ amopfamily => 'brin/bpchar_minmax_multi_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # bloom character
 { amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
   amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
@@ -3007,6 +3092,23 @@
   amoprighttype => 'bit', amopstrategy => '5', amopopr => '>(bit,bit)',
   amopmethod => 'brin' },
 
+# minmax multi bit
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '2', amopopr => '<=(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '3', amopopr => '=(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '4', amopopr => '>=(bit,bit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/bit_minmax_multi_ops', amoplefttype => 'bit',
+  amoprighttype => 'bit', amopstrategy => '5', amopopr => '>(bit,bit)',
+  amopmethod => 'brin' },
+
 # minmax bit varying
 { amopfamily => 'brin/varbit_minmax_ops', amoplefttype => 'varbit',
   amoprighttype => 'varbit', amopstrategy => '1', amopopr => '<(varbit,varbit)',
@@ -3024,6 +3126,23 @@
   amoprighttype => 'varbit', amopstrategy => '5', amopopr => '>(varbit,varbit)',
   amopmethod => 'brin' },
 
+# minmax multi bit varying
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '1', amopopr => '<(varbit,varbit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '2',
+  amopopr => '<=(varbit,varbit)', amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '3', amopopr => '=(varbit,varbit)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '4',
+  amopopr => '>=(varbit,varbit)', amopmethod => 'brin' },
+{ amopfamily => 'brin/varbit_minmax_multi_ops', amoplefttype => 'varbit',
+  amoprighttype => 'varbit', amopstrategy => '5', amopopr => '>(varbit,varbit)',
+  amopmethod => 'brin' },
+
 # minmax numeric
 { amopfamily => 'brin/numeric_minmax_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 51403716b1..69a7ed682c 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,22 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi bytea
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/bytea_minmax_multi_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom bytea
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '1',
@@ -836,6 +852,22 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi "char"
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/char_minmax_multi_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom "char"
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -867,6 +899,22 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi name
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/name_minmax_multi_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom name
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -1197,6 +1245,22 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi text
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/text_minmax_multi_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom text
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -1663,6 +1727,23 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi character
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/bpchar_minmax_multi_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # bloom character
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '1',
@@ -2180,6 +2261,21 @@
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi bit
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/bit_minmax_multi_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # minmax bit varying
 { amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
   amprocrighttype => 'varbit', amprocnum => '1',
@@ -2194,6 +2290,23 @@
   amprocrighttype => 'varbit', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi bit varying
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/varbit_minmax_multi_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+
 # minmax numeric
 { amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index da25befefe..823f1b01fe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,18 +266,27 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_minmax_multi_ops',
+  opcfamily => 'brin/bytea_minmax_multi_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bytea_bloom_ops',
   opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
   opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_minmax_multi_ops',
+  opcfamily => 'brin/char_minmax_multi_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_bloom_ops',
   opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
   opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_minmax_multi_ops',
+  opcfamily => 'brin/name_minmax_multi_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_bloom_ops',
   opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
   opckeytype => 'name', opcdefault => 'f' },
@@ -311,6 +320,9 @@
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_minmax_multi_ops',
+  opcfamily => 'brin/text_minmax_multi_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_bloom_ops',
   opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
   opckeytype => 'text', opcdefault => 'f' },
@@ -381,6 +393,9 @@
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_minmax_multi_ops',
+  opcfamily => 'brin/bpchar_minmax_multi_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
   opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar', opcdefault => 'f' },
@@ -440,9 +455,15 @@
   opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
+{ opcmethod => 'brin', opcname => 'bit_minmax_multi_ops',
+  opcfamily => 'brin/bit_minmax_multi_ops', opcintype => 'bit',
+  opckeytype => 'bit', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
   opcfamily => 'brin/varbit_minmax_ops', opcintype => 'varbit',
   opckeytype => 'varbit' },
+{ opcmethod => 'brin', opcname => 'varbit_minmax_multi_ops',
+  opcfamily => 'brin/varbit_minmax_multi_ops', opcintype => 'varbit',
+  opckeytype => 'varbit', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index ba9231ac8c..ffb20e72ab 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -192,6 +192,8 @@
   opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '9967',
+  opfmethod => 'brin', opfname => 'text_minmax_multi_ops' },
 { oid => '9902',
   opfmethod => 'brin', opfname => 'text_bloom_ops' },
 { oid => '9903',
@@ -210,14 +212,20 @@
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '9968',
+  opfmethod => 'brin', opfname => 'char_minmax_multi_ops' },
 { oid => '9906',
   opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '9969',
+  opfmethod => 'brin', opfname => 'bytea_minmax_multi_ops' },
 { oid => '9907',
   opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '9970',
+  opfmethod => 'brin', opfname => 'name_minmax_multi_ops' },
 { oid => '9908',
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
@@ -260,6 +268,8 @@
   opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '9971',
+  opfmethod => 'brin', opfname => 'bpchar_minmax_multi_ops' },
 { oid => '9915',
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
@@ -276,8 +286,12 @@
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
+{ oid => '9972',
+  opfmethod => 'brin', opfname => 'bit_minmax_multi_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
+{ oid => '9973',
+  opfmethod => 'brin', opfname => 'varbit_minmax_multi_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
 { oid => '9938',
-- 
2.26.2

0009-Ignore-correlation-for-new-BRIN-opclasses-20210215.patchtext/x-patch; charset=UTF-8; name=0009-Ignore-correlation-for-new-BRIN-opclasses-20210215.patchDownload
From 7b645dfab281078c76c191adad4400530b2fe306 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 9/9] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index b54b963f87..af87737ae0 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -410,6 +410,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index c77c207e94..28a6a85b5a 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1798,6 +1798,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 47ca4ddbb5..bf40f9ce32 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index fdaff42722..5fbf8cf9c7 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

#147John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#146)
Re: WIP: BRIN multi-range indexes

On Mon, Feb 15, 2021 at 10:37 AM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

[v20210215]

Hi Tomas,

This time I only looked at the cumulative changes in the multiminmax
opclass, since I'm pretty sure all the bloom issues have been addressed.

* XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
* something ExpandedRange or so.

The time to do it is now, if you like, or remove the XXX. I agree with the
comment, FWIW.

In has_matching_range():

* So we know it's in the general min/max, the question is whether it
* falls in one of the ranges or gaps. We'll use a binary search on
* the ranges.
*
* it's in the general range, but is it actually covered by any
* of the ranges? Repeat the check for each range.
*
* XXX We simply walk the ranges sequentially, but maybe we could
* further leverage the ordering and non-overlap and use bsearch to
* speed this up a bit.

It looks to me like you already implemented binary search and the last part
is out of date, or am I missing something?

Same in range_contains_value():

* XXX This might benefit from the fact that both the intervals and exact
* values are sorted - we might do bsearch or something. Currently that
* does not make much difference (there are only ~32 intervals), but if
* this gets increased and/or the comparator function is more expensive,
* it might be a huge win.

Below that it does binary search if the number of elements > 16.

In merge_combine_ranges():

There are a couple assert-related TODOs.

In brin_minmax_multi_distance_timetz():

* XXX Does this need to consider the time zones?

I wouldn't think so, because the stored values are in UTC -- the time zone
calculation only happens during storage and retrieval, and they've already
been stored, IIUC.

Also, I think you need to copy this part from
brin_minmax_multi_distance_timestamp() here as well:

if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
PG_RETURN_FLOAT8(0);

At this point, I think it's pretty close to commit-ready. I thought maybe I
would create a small index with every type, and make sure it looks sane in
page_inspect, but that's about it.

--
John Naylor
EDB: http://www.enterprisedb.com

#148Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#147)
6 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

attached is a cleaned up version of the patch series, addressing the
issues from the last review. I've also reviewed the comments, merged the
patches to keep only the "hybrid" approach of building minmax-multi, and
a couple minor fixes. I've also passed it through pgindent.

There are a couple minor FIXMEs, I'll deal with those before commit.

I have a couple questions, though:

1) The 0001 patch allows passing of all scan keys to BRIN opclasses,
which is needed for the minmax-multi to work. But it also modifies the
existing opclasses (minmax and inclusion) to do this - but for those
opclasses it does not make much difference, I think. Initially this was
done because the patch did this for all opclasses, but then we added the
detection based on number of parameters. So I wonder if we should just
remove this, to make the patch a bit smaller. It'll also test the other
code path (for opclasses without the last parameter).

2) This needs bsearch_arg, but we only have that defined/used in
extended_statistics_internal.h - it seems silly to include that here, as
this has nothing to do with extended stats, so I simply added another
prototype (which gets linked correctly). But, I suppose a better way
would be to define our "portable" variant pg_bsearch_arg, next to
pg_qsort etc.

On 2/22/21 9:16 PM, John Naylor wrote:

On Mon, Feb 15, 2021 at 10:37 AM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

[v20210215]

Hi Tomas,

This time I only looked at the cumulative changes in the multiminmax
opclass, since I'm pretty sure all the bloom issues have been addressed.

 * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
 * something ExpandedRange or so.

The time to do it is now, if you like, or remove the XXX. I agree with
the comment, FWIW.

OK. I've renamed the struct to ExpandedRanges, I think this is better.
I've also updated names of the various function names containing
_combine_ (usually to _expanded_).

In has_matching_range():

 * So we know it's in the general min/max, the question is whether it
 * falls in one of the ranges or gaps. We'll use a binary search on
 * the ranges.
 *
 * it's in the general range, but is it actually covered by any
 * of the ranges? Repeat the check for each range.
 *
 * XXX We simply walk the ranges sequentially, but maybe we could
 * further leverage the ordering and non-overlap and use bsearch to
 * speed this up a bit.

It looks to me like you already implemented binary search and the last
part is out of date, or am I missing something?

Same in range_contains_value():

 * XXX This might benefit from the fact that both the intervals and exact
 * values are sorted - we might do bsearch or something. Currently that
 * does not make much difference (there are only ~32 intervals), but if
 * this gets increased and/or the comparator function is more expensive,
 * it might be a huge win.

Below that it does binary search if the number of elements > 16.

Good point. I've implemented the binary search a while ago, but I forgot
to update the comments. Fixed.

I'm not sure whether to keep the threshold with 16 elements, or just do
binary search in all cases. A simple linear search tends to be faster
for small arrays, but it depends on how expensive the comparison
function is for the particular data type. Maybe we should keep this
simple for now.

In merge_combine_ranges():

There are a couple assert-related TODOs.

Removed. I think the Assert after the call is sufficient.

In brin_minmax_multi_distance_timetz():

 * XXX Does this need to consider the time zones?

I wouldn't think so, because the stored values are in UTC -- the time
zone calculation only happens during storage and retrieval, and they've
already been stored, IIUC.

Also, I think you need to copy this part from
brin_minmax_multi_distance_timestamp() here as well:

if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
PG_RETURN_FLOAT8(0);

Ummm, in brin_minmax_multi_distance_timetz()? I don't think timetz can
be infinite, no? I think brin_minmax_multi_distance_date you meant?

At this point, I think it's pretty close to commit-ready. I thought
maybe I would create a small index with every type, and make sure it
looks sane in page_inspect, but that's about it. 

OK, thanks for the reviews!

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210303.patchtext/x-patch; charset=UTF-8; name=0001-Pass-all-scan-keys-to-BRIN-consistent-funct-20210303.patchDownload
From a040da89f37965891587f029c107c355168e43ec Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 1/6] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 158 ++++++++++++++++++-----
 src/backend/access/brin/brin_inclusion.c | 140 ++++++++++++++------
 src/backend/access/brin/brin_minmax.c    |  92 ++++++++++---
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 5 files changed, 303 insertions(+), 95 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 27ba596c6e..963b7079cf 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -390,6 +390,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey   **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -411,6 +414,66 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure. We allocate space for all attributes, so
+	 * that we don't have to bother determining which attributes are used.
+	 *
+	 * XXX The widest table can have ~1600 attributes, so this may allocate a
+	 * couple kilobytes of memory). We could invent a more compact approach
+	 * (with just space for used attributes) but that would make the matching
+	 * more complicated, so it may not be a win.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation used in the
+		 * index column (but only if the search is not IS NULL/ IS NOT NULL).
+		 * Otherwise we shouldn't be using this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno - 1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many scan keys
+			 * are there for this attribute, so we simply allocate the largest
+			 * number possible. This may waste a bit of memory, but we only
+			 * expect small number of scan keys in general, so this should be
+			 * negligible, and it's cheaper than having to repalloc
+			 * repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -471,7 +534,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int			attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -481,51 +544,78 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of
+					 * multiple scan keys. We can determine that based on the
+					 * number of arguments - functions with extra parameter
+					 * (number of scan keys) do support this, otherwise we
+					 * have to simply pass the scan keys one by one, as
+					 * before.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same
+						 * for all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet
+						 * the criteria for a single one of them is enough to
+						 * discard the range as a whole, so break out of the
+						 * loop as soon as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+
+							/* mismatching key, no need to look further  */
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..a260074c91 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -251,6 +253,10 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 /*
  * BRIN inclusion consistent function
  *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches all those keys, we check the regular scan keys.
+ *
  * All of the strategies are optional.
  */
 Datum
@@ -258,24 +264,31 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/* Handle IS NULL/IS NOT NULL tests */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -284,7 +297,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -293,7 +311,14 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
@@ -301,10 +326,45 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all regular scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
+	/* This should be called only for regular keys, not for IS [NOT] NULL. */
+	Assert(!(key->sk_flags & SK_ISNULL));
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +384,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +444,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..e116084a02 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -140,29 +142,41 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's min/max
  * values.  Return true if so, false otherwise.
+ *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches all those keys, we check the regular scan keys.
  */
 Datum
 brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
 	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -171,7 +185,12 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -180,13 +199,52 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
+	}
+
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
+
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +287,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..11835d85cd 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 1487710d59..33841e14f2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8191,7 +8191,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8207,7 +8207,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210303.patchtext/x-patch; charset=UTF-8; name=0002-Move-IS-NOT-NULL-handling-from-BRIN-support-20210303.patchDownload
From 1fc7fa0d6aea99124c033aacd8950c534045a3bb Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 2 Mar 2021 19:27:48 +0100
Subject: [PATCH 2/6] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c |  96 +-------
 src/backend/access/brin/brin_minmax.c    |  93 +------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 244 insertions(+), 241 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 963b7079cf..9f2656b8d9 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+								BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -179,7 +182,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -243,31 +245,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -390,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey   **keys;
-	int		   *nkeys;
+	ScanKey   **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -419,13 +399,18 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * consistent support procedure. We allocate space for all attributes, so
 	 * that we don't have to bother determining which attributes are used.
 	 *
+	 * We keep null and regular keys separate, so that we can pass just the
+	 * regular keys to the consistent function easily.
+	 *
 	 * XXX The widest table can have ~1600 attributes, so this may allocate a
 	 * couple kilobytes of memory). We could invent a more compact approach
 	 * (with just space for used attributes) but that would make the matching
 	 * more complicated, so it may not be a win.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -445,23 +430,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno - 1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan keys are
+		 * there for a given attribute, so we simply allocate the largest
+		 * number possible (as if all scan keys belonged to the same
+		 * attribute). This may waste a bit of memory, but we only expect
+		 * small number of scan keys in general, so this should be negligible,
+		 * and it's probably cheaper than having to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many scan keys
-			 * are there for this attribute, so we simply allocate the largest
-			 * number possible. This may waste a bit of memory, but we only
-			 * expect small number of scan keys in general, so this should be
-			 * negligible, and it's cheaper than having to repalloc
-			 * repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -469,9 +454,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -549,15 +548,58 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular and
+					 * IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine
+					 * mismatch within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all
+					 * passed. If there are no regular scan keys, we're done -
+					 * the page range matches. If there are regular keys, but
+					 * the page range is marked as 'all nulls' it can't
+					 * possibly pass (we're assuming the operators are
+					 * strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -703,7 +745,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -732,25 +773,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1529,6 +1553,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1582,3 +1639,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values; our
+	 * deformed tuple will get updated if the new tuple doesn't fit the
+	 * original range (note this means we can't break out of the loop early).
+	 * Make a note of whether this happens, so that we know to insert the
+	 * modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's the
+			 * first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index a260074c91..b17077703c 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -268,52 +258,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* Handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
 
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
-
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -331,9 +278,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -574,37 +520,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index e116084a02..330bed0487 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -156,52 +146,9 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
 
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -215,9 +162,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -307,34 +253,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 78c89a6961..79440ebe7b 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0003-Optimize-allocations-in-bringetbitmap-20210303.patchtext/x-patch; charset=UTF-8; name=0003-Optimize-allocations-in-bringetbitmap-20210303.patchDownload
From a94f9b7a4fcce34f5c9355d31f74cf438e30b7fe Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 2 Mar 2021 19:57:27 +0100
Subject: [PATCH 3/6] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 60 ++++++++++++++++++++++++++--------
 1 file changed, 47 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 9f2656b8d9..1f82e965f9 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -402,15 +405,52 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * We keep null and regular keys separate, so that we can pass just the
 	 * regular keys to the consistent function easily.
 	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly the
+	 * same lifetime, so that's OK.
+	 *
 	 * XXX The widest table can have ~1600 attributes, so this may allocate a
 	 * couple kilobytes of memory). We could invent a more compact approach
 	 * (with just space for used attributes) but that would make the matching
 	 * more complicated, so it may not be a win.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* regular keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* NULL keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -444,9 +484,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -457,17 +497,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0004-BRIN-bloom-indexes-20210303.patchtext/x-patch; charset=UTF-8; name=0004-BRIN-bloom-indexes-20210303.patchDownload
From dee375db7cd2303f278a39eb253fc1939de5b211 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 4/6] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 178 +++++
 doc/src/sgml/ref/create_index.sgml        |  31 +
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 787 ++++++++++++++++++++++
 src/include/access/brin.h                 |   2 +
 src/include/access/brin_internal.h        |   4 +
 src/include/catalog/pg_amop.dat           | 116 ++++
 src/include/catalog/pg_amproc.dat         | 447 ++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 ++
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +-
 src/test/regress/expected/brin_bloom.out  | 428 ++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 376 +++++++++++
 19 files changed, 2534 insertions(+), 6 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 965dcf472c..a45691873c 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -581,6 +581,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default value is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default value is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..4494484b3b
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,787 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test whether it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_page.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001	/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25	/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+
+#define BloomMaxFilterSize \
+	MAXALIGN_DOWN(BLCKSZ - \
+				  (MAXALIGN(SizeOfPageHeaderData + \
+							sizeof(ItemIdData)) + \
+				   MAXALIGN(sizeof(BrinSpecialSpace)) + \
+				   SizeOfBrinTuple))
+
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * To calculate the additional hashes (treating the uint32 hash as an input
+ * value), we use the approach with two hash functions from this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions are calculated using hard-coded seeds.
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes
+ * that are not entirely 0. But while indexes don't support TOAST, the
+ * varlena can still be compressed. So this seems unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high. But even if the false positive rate
+ * exceeds the desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16		flags;
+
+	/* fields for the HASHED phase */
+	uint8		nhashes;		/* number of hash functions */
+	uint32		nbits;			/* number of bits in the bitmap (size) */
+	uint32		nbits_set;		/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+
+}			BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size		len;
+	BloomFilter *filter;
+
+	int			nbits;			/* size of filter / number of bits */
+	int			nbytes;			/* size of filter / number of bytes */
+
+	double		k;				/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	nbits = ceil(-(ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	nbytes = ((nbits + 7) / 8);
+	nbits = nbytes * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * Initially the bloom filter is just zeroes and so very compressible, but
+	 * as we add values it gets more and more random, and so less and less
+	 * compressible. So initially everything fits on the page, but we might
+	 * get surprising failures later - we want to prevent that, so we reject
+	 * bloom filter that are obviously too large.
+	 *
+	 * XXX It's not uncommon to oversize the bloom filter a bit, to defend
+	 * against unexpected data anomalies (parts of table with more distinct
+	 * values per range etc.). But we still need to make sure even the
+	 * oversized filter fits on page, if such need arises.
+	 *
+	 * XXX This check is not perfect, because the index may have multiple
+	 * filters that are small individually, but too large when combined.
+	 */
+	if (nbytes > BloomMaxFilterSize)
+		elog(ERROR, "the bloom filter is too large (%d > %zu)", nbytes,
+			 BloomMaxFilterSize);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * nbits / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so the
+	 * varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + nbytes;
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = nbits;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter * filter, uint32 value, bool *updated)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (!(filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter * filter, uint32 value)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* calculate the two hashes */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (!(filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash), but
+	 * let's keep the array just like inclusion and minman opclasses, for
+	 * consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+}			BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+									uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double		ndistinct;
+	double		maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this case to
+	 * maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a couple of
+	 * safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples, in the
+	 * range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from
+	 * BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to
+				 * return the current page range if the minimum value in the
+				 * range <= scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters for now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing text
+	 * input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 79440ebe7b..8cc4e532e6 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..04d678f96a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,20 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2040,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2062,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2084,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2155,14 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2180,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2202,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2224,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2266,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2288,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2439,20 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2470,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2492,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2548,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2570,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2636,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..6709c8dfea 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..dea9adaf98 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9901',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '9902',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '9903',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9904',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9905',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '9906',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '9907',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '9908',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9909',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9910',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9911',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9912',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9913',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '9914',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '9915',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9916',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9917',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9918',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9919',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 33841e14f2..3b92bb66e5 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8214,6 +8214,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '9920', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '9921', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '9922', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '9923', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '9924', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11387,4 +11407,18 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8959c2f53b..74e279cbf9 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,5 +679,10 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-
+{ oid => '9925',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..32c56a996a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,428 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..e568b9fea2 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9925 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index c77b0d7342..ecd0806718 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 0264a97324..c0d7fa76f1 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..5d499208e3
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,376 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0005-BRIN-minmax-multi-indexes-20210303.patchtext/x-patch; charset=UTF-8; name=0005-BRIN-minmax-multi-indexes-20210303.patchDownload
From b2e2222da0a61c4f7bc37a28bac99490c402994d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Wed, 3 Feb 2021 19:00:00 +0100
Subject: [PATCH 5/6] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2989 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   33 +-
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    8 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 ++++
 src/include/catalog/pg_amproc.dat           |  600 +++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   85 +
 src/include/catalog/pg_type.dat             |    6 +
 src/test/regress/expected/brin_multi.out    |  445 +++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  397 +++
 21 files changed, 5458 insertions(+), 27 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index a45691873c..e1f0f4c0a0 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -612,6 +612,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     8 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..c522bf2285
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2989 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values */
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Sizing the insert buffer - we use 10x the number of values specified
+ * in the reloption, but we cap it to 8192 not to get too large. When
+ * the buffer gets full, we reduce the number of values by half.
+ */
+#define		MINMAX_BUFFER_FACTOR			10
+#define		MINMAX_BUFFER_MIN				256
+#define		MINMAX_BUFFER_MAX				8192
+#define		MINMAX_BUFFER_LOAD_FACTOR		0.5
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+}			MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange; /* number of values per range */
+}			MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0))
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* Cache information that we need quite often. */
+	Oid			typid;
+	Oid			colloid;
+	AttrNumber	attno;
+	FmgrInfo   *cmp;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nsorted;		/* number of sorted values (ranges + points) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/*
+	 * We simply add the values into a large buffer, without any expensive
+	 * steps (sorting, deduplication, ...). The buffer is a multiple of the
+	 * target number of values, so the compaction happen less often,
+	 * amortizing the costs. We keep the actual target and compact to the
+	 * requested number of values at the very end, before serializing to
+	 * on-disk representation.
+	 */
+	/* requested number of values */
+	int			target_maxvalues;
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum		values[FLEXIBLE_ARRAY_MEMBER];
+}			Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, with a simple header
+ * with basic metadata, followed by the boundary values. It has a varlena
+ * header, so can be treated as varlena directly.
+ *
+ * See range_serialize/range_deserialize for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* type of values stored in the data array */
+	Oid			typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+}			SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(int maxvalues, SerializedRanges *range);
+
+
+/*
+ * Used to represent ranges expanded to make merging and combining easier.
+ *
+ * Each expanded range is essentially an interval, represented by min/max
+ * values, along with a flag whether it's a collapsed range (in which case
+ * the min and max values are equal). We have the flag to handle by-ref
+ * data types - we can't simply compare the datums, and this saves some
+ * calls to the type-specific comparator function.
+ */
+typedef struct ExpandedRange
+{
+	Datum		minval;			/* lower boundary */
+	Datum		maxval;			/* upper boundary */
+	bool		collapsed;		/* true if minval==maxval */
+}			ExpandedRange;
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of extended ranges).
+ */
+typedef struct DistanceValue
+{
+	int			index;
+	double		value;
+}			DistanceValue;
+
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+										   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+													uint16 attno, Oid subtype,
+													uint16 strategynum);
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+}			compare_context;
+
+static int	compare_values(const void *a, const void *b, void *arg);
+
+/* FIXME we have this in extended_stats_internal.h, which seems silly to
+ * include here */
+void	   *bsearch_arg(const void *key, const void *base,
+						size_t nmemb, size_t size,
+						int (*compar) (const void *, const void *, void *),
+						void *arg);
+
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+	int			i;
+	Datum		lt;
+
+	for (i = 0; i < (nvalues - 1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i + 1]);
+		Assert(DatumGetBool(lt));
+	}
+}
+#endif
+
+/*
+ * Comprehensive check of the Ranges structure.
+ */
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+
+	/* some basic sanity checks */
+	Assert(ranges->nranges >= 0);
+	Assert(ranges->nsorted >= 0);
+	Assert(ranges->nvalues >= ranges->nsorted);
+	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
+	Assert(ranges->typid != InvalidOid);
+
+	/*
+	 * First the ranges - there are 2*nranges boundary values, and the values
+	 * have to be strictly ordered (equal values would mean the range is
+	 * collapsed, and should be stored as a point). This also guarantees that
+	 * the ranges do not overlap.
+	 */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2 * ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2 * ranges->nranges],
+					 ranges->nsorted);
+
+	/*
+	 * Check that none of the values are not covered by ranges (both sorted
+	 * and unsorted)
+	 */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		compar;
+		int			start,
+					end;
+		Datum		minvalue,
+					maxvalue;
+
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nranges == 0)
+			break;
+
+		minvalue = ranges->values[0];
+		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+		/* smaller than the smallest value in the first range */
+		if (DatumGetBool(compar))
+			continue;
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+		/* larger than the largest value in the last range */
+		if (DatumGetBool(compar))
+			continue;
+
+		start = 0;				/* first range */
+		end = ranges->nranges - 1;	/* last range */
+		while (true)
+		{
+			int			midpoint = (start + end) / 2;
+
+			/* this means we ran out of ranges in the last step */
+			if (start > end)
+				break;
+
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2 * midpoint];
+			maxvalue = ranges->values[2 * midpoint + 1];
+
+			/*
+			 * Is the value smaller than the minval? If yes, we'll recurse to
+			 * the left side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+			{
+				end = (midpoint - 1);
+				continue;
+			}
+
+			/*
+			 * Is the value greater than the minval? If yes, we'll recurse to
+			 * the right side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+			{
+				start = (midpoint + 1);
+				continue;
+			}
+
+			/* hey, we found a matching range */
+			Assert(false);
+		}
+	}
+
+	/* and values in the unsorted part must not be in sorted part */
+	for (i = ranges->nsorted; i < ranges->nvalues; i++)
+	{
+		compare_context cxt;
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nsorted == 0)
+			break;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		Assert(bsearch_arg(&value, &ranges->values[2 * ranges->nranges],
+						   ranges->nsorted, sizeof(Datum),
+						   compare_values, (void *) &cxt) == NULL);
+	}
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidExpandedRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						  Form_pg_attribute attr, ExpandedRange *ranges,
+						  int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+	FmgrInfo   *eq;
+	FmgrInfo   *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum		r;
+		Datum		minval = ranges[i].minval;
+		Datum		maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else					/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e. upper <
+	 * lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges - 1; i++)
+	{
+		Datum		r;
+		Datum		maxval = ranges[i].maxval;
+		Datum		minval = ranges[i + 1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size		len;
+	Ranges	   *ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values); /* fixed header */
+	len += maxvalues * sizeof(Datum);	/* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+
+/*
+ * range_deduplicate_values
+ *		Deduplicate the part with values in the simple points.
+ *
+ * This is meant to be a cheaper way of reducing the size of the ranges. It
+ * does not touch the ranges, and only sorts the other values - it does not
+ * call the distance functions, which may be quite expensive, etc.
+ */
+static void
+range_deduplicate_values(Ranges *range)
+{
+	int			i,
+				n;
+	int			start;
+	compare_context cxt;
+
+	/*
+	 * If there are no unsorted values, we're done (this probably can't
+	 * happen, as we're adding values to unsorted part).
+	 */
+	if (range->nsorted == range->nvalues)
+		return;
+
+	/* sort the values */
+	cxt.colloid = range->colloid;
+	cxt.cmpFn = range->cmp;
+
+	/* how many values to sort? */
+	start = 2 * range->nranges;
+
+	qsort_arg(&range->values[start],
+			  range->nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	n = 1;
+	for (i = 1; i < range->nvalues; i++)
+	{
+		/* same as preceding value, so store it */
+		if (compare_values(&range->values[start + i - 1],
+						   &range->values[start + i],
+						   (void *) &cxt) == 0)
+			continue;
+
+		range->values[start + n] = range->values[start + i];
+
+		n++;
+	}
+
+	/* now all the values are sorted */
+	range->nvalues = n;
+	range->nsorted = n;
+
+	AssertCheckRanges(range, range->cmp, range->colloid);
+}
+
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size		len;
+	int			nvalues;
+	SerializedRanges *serialized;
+	Oid			typid;
+	int			typlen;
+	bool		typbyval;
+
+	int			i;
+	char	   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nsorted >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+	Assert(range->target_maxvalues > 0);
+
+	/* at this point the range should be compacted to the target size */
+	Assert(2 * range->nranges + range->nvalues <= range->target_maxvalues);
+
+	Assert(range->target_maxvalues <= range->maxvalues);
+
+	/* range boundaries are always sorted */
+	Assert(range->nvalues >= range->nsorted);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2 * range->nranges + range->nvalues;
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges, data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)			/* varlena */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)		/* cstring */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else						/* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->target_maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation this
+	 * depends on the particular data type).
+	 */
+	ptr = serialized->data;		/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int			tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int			tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + len));
+	}
+
+	/* exact size */
+	Assert(ptr == ((char *) serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(int maxvalues, SerializedRanges *serialized)
+{
+	int			i,
+				nvalues;
+	char	   *ptr;
+	bool		typbyval;
+	int			typlen;
+
+	Ranges	   *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2 * serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+	Assert(serialized->maxvalues <= maxvalues);
+
+	range = minmax_multi_init(maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->nsorted = serialized->nvalues;
+	range->maxvalues = maxvalues;
+	range->target_maxvalues = serialized->maxvalues;
+
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need to copy
+	 * the values and will instead just point the values to the serialized
+	 * varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *) serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+/*
+ * compare_expanded_ranges
+ *	  Compare the expanded ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_expanded_ranges(const void *a, const void *b, void *arg)
+{
+	ExpandedRange *ra = (ExpandedRange *) a;
+	ExpandedRange *rb = (ExpandedRange *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * compare_values
+ *	  Compare the values.
+ */
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum	   *da = (Datum *) a;
+	Datum	   *db = (Datum *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *da, *db);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *db, *da);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Check if the new value matches one of the existing ranges.
+ */
+static bool
+has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges,
+				   Datum newval, AttrNumber attno, Oid typid)
+{
+	Datum		compar;
+
+	Datum		minvalue = ranges->values[0];
+	Datum		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+
+	/* binary search on ranges */
+	int			start,
+				end;
+
+	if (ranges->nranges == 0)
+		return false;
+
+	/*
+	 * Otherwise, need to compare the new value with boundaries of all the
+	 * ranges. First check if it's less than the absolute minimum, which is
+	 * the first value in the array.
+	 */
+	cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												   BTLessStrategyNumber);
+	compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+	/* smaller than the smallest value in the range list */
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * And now compare it to the existing maximum (last value in the data
+	 * array). But only if we haven't already ruled out a possible match in
+	 * the minvalue check.
+	 */
+	cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													  BTGreaterStrategyNumber);
+	compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * So we know it's in the general min/max, the question is whether it
+	 * falls in one of the ranges or gaps. We'll do a binary search on the on
+	 * the individual ranges - for each range we check equality (value falls
+	 * into the range), and then check ranges either above or below the
+	 * current range.
+	 */
+	start = 0;					/* first range */
+	end = (ranges->nranges - 1);	/* last range */
+	while (true)
+	{
+		int			midpoint = (start + end) / 2;
+
+		/* this means we ran out of ranges in the last step */
+		if (start > end)
+			return false;
+
+		/* copy the min/max values from the ranges */
+		minvalue = ranges->values[2 * midpoint];
+		maxvalue = ranges->values[2 * midpoint + 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in this range */
+		if (DatumGetBool(compar))
+		{
+			end = (midpoint - 1);
+			continue;
+		}
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+		/* larger than the largest value in this range */
+		if (DatumGetBool(compar))
+		{
+			start = (midpoint + 1);
+			continue;
+		}
+
+		/* hey, we found a matching range */
+		return true;
+	}
+
+	return false;
+}
+
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX We only inspect the sorted parts, which means we may add duplicate
+ * values. It may produce some false negatives when adding the values, but
+ * only after we already added some values (otherwise there is no unsorted
+ * part). And when querying the index, there should be no unsorted values,
+ * because the values are sorted and deduplicated during serialization.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+					 AttrNumber attno, Form_pg_attribute attr,
+					 Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid))
+		return true;
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													BTEqualStrategyNumber);
+
+	/*
+	 * There is no matching range, so let's inspect the sorted values.
+	 *
+	 * XXX We do a sequential search for small number of values, and binary
+	 * search once we have more than 16 values. This threshold is somewhat
+	 * arbitrary, as it depends on how expensive the comparison function is.
+	 * So maybe we should just do the binary search all the time.
+	 *
+	 * XXX If we use the threshold here, maybe we should do the same thing in
+	 * has_matching_range?
+	 */
+	if (ranges->nsorted >= 16)
+	{
+		compare_context cxt;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		if (bsearch_arg(&newval, &ranges->values[2 * ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) != NULL)
+			return true;
+	}
+	else
+	{
+		for (i = 2 * ranges->nranges; i < 2 * ranges->nranges + ranges->nsorted; i++)
+		{
+			Datum		compar;
+
+			compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+			/* found an exact match */
+			if (DatumGetBool(compar))
+				return true;
+		}
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * Expand ranges from Ranges into ExpandedRange array. This expects the
+ * eranges to be pre-allocated and with the correct size - there needs to be
+ * (nranges + nvalues) elements.
+ *
+ * The order of expanded ranges is arbitrary. We do expand the ranges first,
+ * and this part is sorted. But then we expand the values, and this part may
+ * be unsorted.
+ */
+static void
+fill_expanded_ranges(ExpandedRange *eranges, int neranges, Ranges *ranges)
+{
+	int			idx;
+	int			i;
+
+	/* Check that the output array has the right size. */
+	Assert(neranges == (2 * ranges->nranges + ranges->nvalues));
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * i];
+		eranges[idx].maxval = ranges->values[2 * i + 1];
+		eranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].maxval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	/* Did we produce the expected number of elements? */
+	Assert(idx == neranges);
+
+	return;
+}
+
+/*
+ * Sort and deduplicate expanded ranges.
+ *
+ * The ranges may be deduplicated - we're simply appending values, without
+ * checking for duplicates etc. So maybe the deduplication will reduce the
+ * number of ranges enough, and we won't have to compute the distances etc.
+ *
+ * Returns the number of expanded ranges.
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that some of the input data is already sorted (all the ranges
+ * and possibly some of the points) and do merge sort.
+ */
+static int
+sort_expanded_ranges(FmgrInfo *cmp, Oid colloid,
+					 ExpandedRange *eranges, int neranges)
+{
+	int			n;
+	int			i;
+	compare_context cxt;
+
+	Assert(neranges > 0);
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(eranges, neranges, sizeof(ExpandedRange),
+			  compare_expanded_ranges, (void *) &cxt);
+
+	/*
+	 * Deduplicate the ranges - simply compare each range to the preceding
+	 * one, and skip the duplicate ones.
+	 */
+	n = 1;
+	for (i = 1; i < neranges; i++)
+	{
+		/* if the current range is equal to the preceding one, do nothing */
+		if (!compare_expanded_ranges(&eranges[i - 1], &eranges[i], (void *) &cxt))
+			continue;
+
+		/* otherwise copy it to n-th place (if not already there) */
+		if (i != n)
+			memcpy(&eranges[n], &eranges[i], sizeof(ExpandedRange));
+
+		n++;
+	}
+
+	Assert((n > 0) && (n <= neranges));
+
+	return n;
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the expanded ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_overlapping_ranges(FmgrInfo *cmp, Oid colloid,
+						 ExpandedRange *eranges, int neranges)
+{
+	int			idx;
+
+	/* Merge ranges (idx) and (idx+1) if they overlap. */
+	idx = 0;
+	while (idx < (neranges - 1))
+	{
+		Datum		r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap if (minval
+		 * < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges are
+		 * ordered, so there are no more overlaps, because all the remaining
+		 * ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].maxval);
+
+		if (DatumGetBool(r))
+			eranges[idx].maxval = eranges[idx + 1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of the
+		 * previous state).
+		 */
+		eranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the remaining
+		 * ranges by 1. There are ncranges elements, and we need to move
+		 * elements from (idx+2). That means the number of elements to move is
+		 * [ncranges - (idx+2)].
+		 */
+		memmove(&eranges[idx + 1], &eranges[idx + 2],
+				(neranges - (idx + 2)) * sizeof(ExpandedRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range, as
+		 * it might overlap with additional ranges thanks to the merge).
+		 */
+		neranges--;
+	}
+
+	return neranges;
+}
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ * This is intentionally sorting the distances in descending order, i.e.
+ * the longer gaps will be at the front.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *) a;
+	DistanceValue *db = (DistanceValue *) b;
+
+	if (da->value < db->value)
+		return 1;
+	else if (da->value > db->value)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Given an array of expanded ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (max-min) for pairs
+ * of consecutive ranges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_expanded_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			ndistances;
+	DistanceValue *distances;
+
+	Assert(neranges >= 2);
+
+	ndistances = (neranges - 1);
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * ndistances);
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges so
+	 * that we can sort them once.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		Datum		a1,
+					a2,
+					r;
+
+		a1 = eranges[i].maxval;
+		a2 = eranges[i + 1].minval;
+
+		/* compute length of the gap (between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index of the gap the distance is for */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/*
+	 * Sort the distances in descending order, so that the longest gaps are at
+	 * the front.
+	 */
+	pg_qsort(distances, ndistances, sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds expanded ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).  This expanded
+ * representation makes the processing a bit easier, as it allows handling
+ * ranges and points the same way.
+ *
+ * We sort and deduplicate the expanded ranges - this is necessary, because
+ * the points may be unsorted. And moreover the two parts (ranges and
+ * points) are sorted on their own.
+ */
+static ExpandedRange *
+build_expanded_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					  int *nranges)
+{
+	int			neranges;
+	ExpandedRange *eranges;
+
+	/* both ranges and points are expanded into a separate element */
+	neranges = ranges->nranges + ranges->nvalues;
+
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges */
+	fill_expanded_ranges(eranges, neranges, ranges);
+
+	/* sort and deduplicate the expanded ranges */
+	neranges = sort_expanded_ranges(cmp, colloid, eranges, neranges);
+
+	/* remember how many cranges we built */
+	*nranges = neranges;
+
+	return eranges;
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(ExpandedRange *cranges, int ncranges)
+{
+	int			i;
+	int			count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+#endif
+
+/*
+ * reduce_expanded_ranges
+ *		reduce the ranges until the number of values is low enough
+ *
+ * Combines ranges until the number of boundary values drops below the
+ * threshold specified by max_values. This happens by merging enough
+ * ranges by distance between them.
+ *
+ * Returns the number of result ranges.
+ *
+ * We simply use the global min/max and then add boundaries for enough
+ * largest gaps. Each gap adds 2 values, so we simply use (target/2-1)
+ * distances. Then we simply sort all the values - each two values are
+ * a boundary of a range (possibly collapsed).
+ *
+ * XXX Some of the ranges may be collapsed (i.e. the min/max values are
+ * equal), but we ignore that for now. We could repeat the process,
+ * adding a couple more gaps recursively.
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ *
+ * XXX Or maybe we could just handle this by using random value as a
+ * tie-break, or by adding random noise to the actual distance.
+ */
+static int
+reduce_expanded_ranges(ExpandedRange *eranges, int neranges,
+					   DistanceValue *distances, int max_values,
+					   FmgrInfo *cmp, Oid colloid)
+{
+	int			i;
+	int			nvalues;
+	Datum	   *values;
+
+	compare_context cxt;
+
+	/* total number of gaps between ranges */
+	int			ndistances = (neranges - 1);
+
+	/* number of gaps to keep */
+	int			keep = (max_values / 2 - 1);
+
+	/*
+	 * Maybe we have sufficiently low number of ranges already?
+	 *
+	 * XXX This should happen before we actually do the expensive stuff like
+	 * sorting, so maybe this should be just an assert.
+	 */
+	if (keep >= ndistances)
+		return neranges;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	/* add the global min/max values, from the first/last range */
+	values[nvalues++] = eranges[0].minval;
+	values[nvalues++] = eranges[neranges - 1].maxval;
+
+	/* add boundary values for enough gaps */
+	for (i = 0; i < keep; i++)
+	{
+		/* index of the gap between (index) and (index+1) ranges */
+		int			index = distances[i].index;
+
+		Assert((index >= 0) && ((index + 1) < neranges));
+
+		/* add max from the preceding range, minval from the next one */
+		values[nvalues++] = eranges[index].maxval;
+		values[nvalues++] = eranges[index + 1].minval;
+
+		Assert(nvalues <= max_values);
+	}
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/*
+	 * Sort the values using the comparator function, and form ranges from the
+	 * sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		eranges[i].minval = values[2 * i];
+		eranges[i].maxval = values[2 * i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		eranges[i].collapsed = (compare_values(&values[2 * i],
+											   &values[2 * i + 1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+/*
+ * Store the boundary values from ExpandedRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_expanded_ranges(Ranges *ranges, ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (!eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->values[idx++] = eranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+
+	/* all the values are sorted */
+	ranges->nsorted = ranges->nvalues;
+
+	Assert(count_values(eranges, neranges) == 2 * ranges->nranges + ranges->nvalues);
+	Assert(2 * ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+}
+
+
+/*
+ * Consider freeing space in the ranges.
+ *
+ * Returns true if the value was actually modified.
+ */
+static bool
+ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *range)
+{
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	/*
+	 * If there is free space in the buffer, we're done without having to
+	 * modify anything.
+	 */
+	if (2 * range->nranges + range->nvalues < range->maxvalues)
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* did we reduce enough free space by just the deduplication? */
+	if (2 * range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR)
+		return true;
+
+	/*
+	 * We need to combine some of the existing ranges, to reduce the number of
+	 * values we have to store.
+	 *
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it (we might have
+	 * to do this repeatedly, even for a single BRIN page range). Otherwise
+	 * we'd have problems e.g. when building new indexes. So we use a memory
+	 * context and make sure we free the memory at the end (so if we call the
+	 * distance function many times, it might be an issue, but meh).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, colloid, range, &neranges);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not use
+	 * too low or high value.
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR,
+									  cmpFn, colloid);
+
+	/* Make sure we've sufficiently reduced the number of ranges. */
+	Assert(count_values(eranges, neranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR);
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(range, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Did we break the ranges somehow? */
+	AssertCheckRanges(range, cmpFn, colloid);
+
+	return true;
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn;
+	bool		modified = false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Make sure there's enough free space in the buffer. We only trigger this
+	 * when the buffer is full, which means it had to be modified as we size
+	 * it to be larger than what is stored on disk.
+	 *
+	 * XXX This needs to happen before we check if the value is contained in
+	 * the range, because the value might be in the unsorted part, and we
+	 * don't check that in range_contains_value. The deduplication would then
+	 * move it to the sorted part, and we'd add the value too, which violates
+	 * the rule that we never have duplicates with the ranges or sorted
+	 * values.
+	 *
+	 * XXX At the moment this only does the deduplication.
+	 *
+	 * XXX We might also deduplicate and recheck if the value is contained,
+	 * but that seems like an overkill. We'd need to deduplicate anyway, so
+	 * why not do it now.
+	 */
+	modified = ensure_free_space_in_buffer(bdesc, colloid,
+										   attno, attr, ranges);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then do the
+	 * deduplication in a batch, hoping for better efficiency. But that would
+	 * mean we actually modify the range every time, which means having to
+	 * serialize the value, which does palloc, walks the values, copies them,
+	 * etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming the
+	 * comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return modified;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we do a
+	 * simple insertion sort. We could do something more elaborate, e.g. by
+	 * sorting the values only now and then, but for small counts (e.g. when
+	 * maxvalues is 64) this should be fine.
+	 */
+	ranges->values[2 * ranges->nranges + ranges->nvalues] = newval;
+	ranges->nvalues++;
+
+	/*
+	 * Check we haven't broken the ordering of boundary values (checks both
+	 * parts, but that doesn't hurt).
+	 */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/* Also check the range contains the value we just added. */
+	/* FIXME Assert(ranges, cmpFn, colloid); */
+
+	/* yep, we've modified the range */
+	return true;
+}
+
+/*
+ * Generate range representation of data collected during "batch mode".
+ * This is similar to reduce_expanded_ranges, except that we can't assume
+ * the values are sorted and there may be duplicate values.
+ */
+static void
+compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
+											   BTLessStrategyNumber);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, ranges->attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, ranges->colloid, ranges, &neranges);
+
+	/* FIXME this should do neranges >= max_values */
+	if (neranges > 1)
+	{
+		/* build array of gap distances and sort them in ascending order */
+		distances = build_distances(distanceFn, ranges->colloid,
+									eranges, neranges);
+
+		/*
+		 * Combine ranges until we get below max_values. We don't use any
+		 * scale factor, because this is used during serialization, and we
+		 * don't expect more tuples to be inserted anytime soon.
+		 */
+		neranges = reduce_expanded_ranges(eranges, neranges, distances,
+										  max_values, cmpFn, ranges->colloid);
+
+		Assert(count_values(eranges, neranges) <= max_values);
+	}
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(ranges, eranges, neranges);
+
+	/* check all the range invariants */
+	AssertCheckRanges(ranges, cmpFn, ranges->colloid);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float		a1 = PG_GETARG_FLOAT4(0);
+	float		a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double		a1 = PG_GETARG_FLOAT8(0);
+	double		a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16		a1 = PG_GETARG_INT16(0);
+	int16		a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32		a1 = PG_GETARG_INT32(0);
+	int32		a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64		a1 = PG_GETARG_INT64(0);
+	int64		a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double		da1,
+				da2;
+
+	ItemPointer pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	/*
+	 * We use the no-check variants here, because user-supplied values may
+	 * have (ip_posid == 0). See ItemPointerCompare.
+	 */
+	da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa1);
+
+	da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum		d;
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int			i;
+	float8		delta = 0;
+
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN - 1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute approximate distance between two dates.
+ */
+Datum
+brin_minmax_multi_distance_date(PG_FUNCTION_ARGS)
+{
+	DateADT		dateVal1 = PG_GETARG_DATEADT(0);
+	DateADT		dateVal2 = PG_GETARG_DATEADT(1);
+
+	if (DATE_NOT_FINITE(dateVal1) || DATE_NOT_FINITE(dateVal2))
+		PG_RETURN_FLOAT8(0);
+
+	PG_RETURN_FLOAT8(dateVal1 - dateVal2);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeADT		ta = PG_GETARG_TIMEADT(0);
+	TimeADT		tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+Datum
+brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Timestamp	dt1 = PG_GETARG_TIMESTAMP(0);
+	Timestamp	dt2 = PG_GETARG_TIMESTAMP(1);
+
+	if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
+		PG_RETURN_FLOAT8(0);
+
+	delta = dt2 - dt1;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Interval   *ia = PG_GETARG_INTERVAL_P(0);
+	Interval   *ib = PG_GETARG_INTERVAL_P(1);
+	Interval   *result;
+
+	result = (Interval *) palloc(sizeof(Interval));
+
+	result->month = ib->month - ia->month;
+	/* overflow check copied from int4mi */
+	if (!SAMESIGN(ib->month, ia->month) &&
+		!SAMESIGN(result->month, ib->month))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->day = ib->day - ia->day;
+	if (!SAMESIGN(ib->day, ia->day) &&
+		!SAMESIGN(result->day, ib->day))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->time = ib->time - ia->time;
+	if (!SAMESIGN(ib->time, ia->time) &&
+		!SAMESIGN(result->time, ib->time))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	/*
+	 * We assume months have 31 days - we don't need to be precise, in the
+	 * worst case we'll build somewhat less efficient ranges.
+	 */
+	delta = (float8) (result->month * 31 + result->day);
+
+	/* convert to microseconds (just like the time part) */
+	delta = 24L * 3600L * delta;
+
+	/* and add the time part */
+	delta += result->time / (float8) 1000000.0;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((float8) b->h - (float8) a->h);
+	delta /= 256;
+
+	delta += ((float8) b->g - (float8) a->g);
+	delta /= 256;
+
+	delta += ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+	int			i;
+	int			len;
+	unsigned char *addra,
+			   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be in
+	 * maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		PG_RETURN_FLOAT8(1.0);
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16;				/* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len - 1; i >= 0; i--)
+	{
+		delta += (float8) addrb[i] - (float8) addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
+{
+	Ranges	   *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s;
+
+	/*
+	 * In batch mode, we need to compress the accumulated values to the
+	 * actually requested number of values/ranges.
+	 */
+	compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+
+	s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges	   *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 *
+	 * When starting with an empty range, we assume this is a batch mode, i.e.
+	 * we size the buffer for the maximum possible number of items in the
+	 * range (based on range size and max number of items on a page).
+	 *
+	 * XXX This may require quite a bit of memory, so maybe we should use some
+	 * value in between. OTOH most tables will have much wider rows, so the
+	 * number of rows per page is much lower.
+	 *
+	 * XXX Maybe we should do this (using larger buffer) even when there
+	 * already is a summary?
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		int			target_maxvalues;
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		/* what was specified as a reloption? */
+		target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, target_maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+		ranges = minmax_multi_init(maxvalues);
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+		ranges->target_maxvalues = target_maxvalues;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, serialized->maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		ranges = range_deserialize(maxvalues, serialized);
+
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+
+	/* MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS(); */
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges	   *ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized->maxvalues, serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum		minval = ranges->values[2 * rangeno];
+		Datum		maxval = ranges->values[2 * rangeno + 1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+					{
+						Datum		compar;
+						FmgrInfo   *cmpFn;
+
+						/* by default this range does not match */
+						matches = false;
+
+						/*
+						 * Otherwise, need to compare the new value with
+						 * boundaries of all the ranges. First check if it's
+						 * less than the absolute minimum, which is the first
+						 * value in the array.
+						 */
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTLessStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+						/* smaller than the smallest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTGreaterStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+						/* larger than the largest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						/*
+						 * haven't managed to eliminate this range, so
+						 * consider it matching
+						 */
+						matches = true;
+
+						break;
+					}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		val = ranges->values[2 * ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+
+	/* MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS(); */
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges *serialized_a;
+	SerializedRanges *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	ExpandedRange *eranges;
+	int			neranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue *distances;
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a);
+	ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	neranges = (ranges_a->nranges + ranges_a->nvalues) +
+		(ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges with entries for the first range */
+	fill_expanded_ranges(eranges, ranges_a->nranges + ranges_a->nvalues,
+						 ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_expanded_ranges(&eranges[ranges_a->nranges + ranges_a->nvalues],
+						 ranges_b->nranges + ranges_b->nvalues,
+						 ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* sort the expanded ranges */
+	sort_expanded_ranges(cmpFn, colloid, eranges, neranges);
+
+	/*
+	 * We've loaded two different lists of expanded ranges, so some of them
+	 * may be overlapping. So walk through them and merge them.
+	 */
+	neranges = merge_overlapping_ranges(cmpFn, colloid, eranges, neranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidExpandedRanges(bdesc, colloid, attno, attr, eranges, neranges);
+
+	/*
+	 * If needed, reduce some of the ranges.
+	 *
+	 * XXX This may be fairly expensive, so maybe we should do it only when
+	 * it's actually needed (when we have too many ranges).
+	 */
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the threshold. The
+	 * collapsed ranges will be stored as a single value.
+	 *
+	 * XXX This does not apply the load factor, as we don't expect to add more
+	 * values to the range, so we prefer to keep as many ranges as possible.
+	 *
+	 * XXX Can the maxvalues be different in the two ranges? Perhaps we should
+	 * use maximum of those?
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  ranges_a->maxvalues,
+									  cmpFn, colloid);
+
+	/* update the first range summary */
+	store_expanded_ranges(ranges_a, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+								   uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 8, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges	   *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges->maxvalues, ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum		a,
+					b;
+		text	   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum		a;
+		text	   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..69ce6dfca4 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(brdesc,
+												  tuple->bt_columns[keyno].bv_mem_value,
+												  tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -169,15 +177,15 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
 		{
-			Datum value = tuple->bt_columns[keyno].bv_values[datumno];
+			Datum		value = tuple->bt_columns[keyno].bv_values[datumno];
 
 #ifdef TOAST_INDEX_HACK
 
 			/* We must look at the stored type, not at the index descriptor. */
-			TypeCacheEntry	*atttype = brdesc->bd_info[keyno]->oi_typcache[datumno];
+			TypeCacheEntry *atttype = brdesc->bd_info[keyno]->oi_typcache[datumno];
 
 			/* Do we need to free the value at the end? */
-			bool free_value = false;
+			bool		free_value = false;
 
 			/* For non-varlena types we don't need to do anything special */
 			if (atttype->typlen != -1)
@@ -193,9 +201,9 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 * If value is stored EXTERNAL, must fetch it so we are not
 			 * depending on outside storage.
 			 *
-			 * XXX Is this actually true? Could it be that the summary is
-			 * NULL even for range with non-NULL data? E.g. degenerate bloom
-			 * filter may be thrown away, etc.
+			 * XXX Is this actually true? Could it be that the summary is NULL
+			 * even for range with non-NULL data? E.g. degenerate bloom filter
+			 * may be thrown away, etc.
 			 */
 			if (VARATT_IS_EXTERNAL(DatumGetPointer(value)))
 			{
@@ -205,8 +213,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			}
 
 			/*
-			 * If value is above size target, and is of a compressible datatype,
-			 * try to compress it in-line.
+			 * If value is above size target, and is of a compressible
+			 * datatype, try to compress it in-line.
 			 */
 			if (!VARATT_IS_EXTENDED(DatumGetPointer(value)) &&
 				VARSIZE(DatumGetPointer(value)) > TOAST_INDEX_TARGET &&
@@ -495,6 +503,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +587,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 8cc4e532e6..fdaff42722 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..87de94f397 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,11 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+/*
+ * The BRIN opclasses may register serialization callback, in case the on-disk
+ * and in-memory representations differ (e.g. for performance reasons).
+ */
+typedef void (*brin_serialize_callback_type) (BrinDesc *bdesc, Datum src, Datum *dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +32,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 82e874130d..654584a03f 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 04d678f96a..8135854163 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2062,6 +2208,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2088,6 +2251,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2155,6 +2334,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2180,6 +2425,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2202,6 +2464,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2224,6 +2503,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2288,6 +2584,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2439,6 +2752,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2470,6 +2929,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2492,6 +2968,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2548,6 +3041,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2570,6 +3080,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2636,6 +3163,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 6709c8dfea..51403716b1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index dea9adaf98..ba9231ac8c 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9926',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '9901',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9927',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '9902',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9928',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '9904',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9929',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '9905',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9930',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '9909',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '9910',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9931',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9932',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '9911',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9933',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '9912',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9934',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '9913',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9935',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '9914',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9936',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '9916',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9937',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '9917',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9938',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '9918',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9939',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '9919',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3b92bb66e5..728763b97e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8197,6 +8197,77 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9940', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9941', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9942', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9943', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9944', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9945', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9946', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9947', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9948', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9949', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9950', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9951', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9952', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9953', descr => 'BRIN multi minmax date distance',
+  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
+{ oid => '9954', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9955', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9956', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9957', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9958', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9959', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9960', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+{ oid => '9961', descr => 'BRIN multi minmax timestamp distance',
+  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11421,4 +11492,18 @@
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
 
+{ oid => '9962', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9963', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9964', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9965', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 74e279cbf9..e809094490 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -685,4 +685,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+{ oid => '9966',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..e13cb59c7e
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,445 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+ERROR:  value 7 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+DROP INDEX brinidx_multi;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index e568b9fea2..0541c12a25 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9925 | pg_brin_bloom_summary
-(5 rows)
+ 9966 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ecd0806718..d34fc7ef88 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index c0d7fa76f1..91834a984e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..6d61fb84c6
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,397 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+
+DROP INDEX brinidx_multi;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0006-Ignore-correlation-for-new-BRIN-opclasses-20210303.patchtext/x-patch; charset=UTF-8; name=0006-Ignore-correlation-for-new-BRIN-opclasses-20210303.patchDownload
From 1f1532ece8ecd3c49075ad85e06e951f5ff343da Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 6/6] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 4494484b3b..46ffc068be 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -412,6 +412,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index c522bf2285..98382df3c7 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1744,6 +1744,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 52314d3aa1..0320d128f6 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index fdaff42722..5fbf8cf9c7 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

#149Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Tomas Vondra (#148)
Re: WIP: BRIN multi-range indexes

On 2021-Mar-03, Tomas Vondra wrote:

1) The 0001 patch allows passing of all scan keys to BRIN opclasses,
which is needed for the minmax-multi to work. But it also modifies the
existing opclasses (minmax and inclusion) to do this - but for those
opclasses it does not make much difference, I think. Initially this was
done because the patch did this for all opclasses, but then we added the
detection based on number of parameters. So I wonder if we should just
remove this, to make the patch a bit smaller. It'll also test the other
code path (for opclasses without the last parameter).

I think it makes sense to just do them all in one pass. I think trying
to keep the old way working just because it's how it was working
previously does not have much benefit. I don't think we care about the
*patch* being small as much as the resulting *code* being as simple as
possible (but no simpler).

2) This needs bsearch_arg, but we only have that defined/used in
extended_statistics_internal.h - it seems silly to include that here, as
this has nothing to do with extended stats, so I simply added another
prototype (which gets linked correctly). But, I suppose a better way
would be to define our "portable" variant pg_bsearch_arg, next to
pg_qsort etc.

Yeah, I think it makes sense to move bsearch_arg to a place where it's
more generally accesible (src/port/bsearch_arg.c I suppose), and make
both places use that.

--
�lvaro Herrera Valdivia, Chile

#150Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Alvaro Herrera (#149)
Re: WIP: BRIN multi-range indexes

On 3/3/21 12:44 AM, Alvaro Herrera wrote:

On 2021-Mar-03, Tomas Vondra wrote:

1) The 0001 patch allows passing of all scan keys to BRIN opclasses,
which is needed for the minmax-multi to work. But it also modifies the
existing opclasses (minmax and inclusion) to do this - but for those
opclasses it does not make much difference, I think. Initially this was
done because the patch did this for all opclasses, but then we added the
detection based on number of parameters. So I wonder if we should just
remove this, to make the patch a bit smaller. It'll also test the other
code path (for opclasses without the last parameter).

I think it makes sense to just do them all in one pass. I think trying
to keep the old way working just because it's how it was working
previously does not have much benefit. I don't think we care about the
*patch* being small as much as the resulting *code* being as simple as
possible (but no simpler).

That's kinda my point - I agree the size of the patch is not the primary
concern, but it makes the minmax/inclusion code a bit more complicated
(because they now have to loop over the keys), with very little benefit
(there might be some speedup, but IMO it's rather negligible).

Alternatively we could simply remove the code supporting the old API
with "consistent" functions without the additional parameter. But the
idea was to seamlessly support existing opclasses / not breaking them
unnecessarily (I know we don't guarantee that in major upgrades, but as
they may not benefit from this, why break them?). It'd simplify the code
in brin.c a little bit, but the opclasses a bit more complex.

2) This needs bsearch_arg, but we only have that defined/used in
extended_statistics_internal.h - it seems silly to include that here, as
this has nothing to do with extended stats, so I simply added another
prototype (which gets linked correctly). But, I suppose a better way
would be to define our "portable" variant pg_bsearch_arg, next to
pg_qsort etc.

Yeah, I think it makes sense to move bsearch_arg to a place where it's
more generally accesible (src/port/bsearch_arg.c I suppose), and make
both places use that.

OK, will do.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#151Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Tomas Vondra (#150)
Re: WIP: BRIN multi-range indexes

On 2021-Mar-03, Tomas Vondra wrote:

That's kinda my point - I agree the size of the patch is not the primary
concern, but it makes the minmax/inclusion code a bit more complicated
(because they now have to loop over the keys), with very little benefit
(there might be some speedup, but IMO it's rather negligible).

Yeah, OK.

Alternatively we could simply remove the code supporting the old API
with "consistent" functions without the additional parameter. But the
idea was to seamlessly support existing opclasses / not breaking them
unnecessarily (I know we don't guarantee that in major upgrades, but as
they may not benefit from this, why break them?). It'd simplify the code
in brin.c a little bit, but the opclasses a bit more complex.

Well, I doubt any opclass-support functions exist outside of core.
Or am I just outdated and we do know of some?

--
�lvaro Herrera Valdivia, Chile
"Escucha y olvidar�s; ve y recordar�s; haz y entender�s" (Confucio)

#152Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Alvaro Herrera (#151)
Re: WIP: BRIN multi-range indexes

On 3/3/21 1:17 AM, Alvaro Herrera wrote:

On 2021-Mar-03, Tomas Vondra wrote:

That's kinda my point - I agree the size of the patch is not the
primary concern, but it makes the minmax/inclusion code a bit more
complicated (because they now have to loop over the keys), with
very little benefit (there might be some speedup, but IMO it's
rather negligible).

Yeah, OK.

Alternatively we could simply remove the code supporting the old
API with "consistent" functions without the additional parameter.
But the idea was to seamlessly support existing opclasses / not
breaking them unnecessarily (I know we don't guarantee that in
major upgrades, but as they may not benefit from this, why break
them?). It'd simplify the code in brin.c a little bit, but the
opclasses a bit more complex.

Well, I doubt any opclass-support functions exist outside of core. Or
am I just outdated and we do know of some?

I'm not aware of any. This was proposed by Nikita Glukhov:

/messages/by-id/49cb668f-d6f9-3493-681d-7d40b715ef64@postgrespro.ru

Alexander Korotkov seemed to agree with Nikita, but I don't recall any
references to actual existing opclasses.

Then again - the amount of code to support two signatures is fairly
small. If we decide to get rid of it, then fine - but the new complexity
in minmax/inclusion likely exceeds that.

Which is why I was thinking about still supporting both signatures, but
reverting the changes in brin_minmax and brin_inclusion.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#153John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#148)
Re: WIP: BRIN multi-range indexes

On Tue, Mar 2, 2021 at 7:32 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

Ummm, in brin_minmax_multi_distance_timetz()? I don't think timetz can
be infinite, no? I think brin_minmax_multi_distance_date you meant?

In timestamp.c there are plenty of checks for TIMESTAMP_NOT_FINITE
for TimestampTz types and I assumed that was applicable here.

--
John Naylor
EDB: http://www.enterprisedb.com

#154Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#153)
Re: WIP: BRIN multi-range indexes

On 3/4/21 5:30 PM, John Naylor wrote:

On Tue, Mar 2, 2021 at 7:32 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

Ummm, in brin_minmax_multi_distance_timetz()? I don't think timetz can
be infinite, no? I think brin_minmax_multi_distance_date you meant?

In timestamp.c there are plenty of checks for TIMESTAMP_NOT_FINITE
for TimestampTz types and I assumed that was applicable here.

I don't think so. The NOT_FINITE macros are defined only for timestamps
and dates, not for TimeTzADT. So I think the current code is correct.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#155Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#152)
8 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

Here is an updated version of the patch series, with a couple minor
changes/improvements.

1) adding bsearch_arg to src/port/

2) moving minmax/inclusion changes from 0002 to a separate patch 0003

I think we should either ditch the 0003 (i.e. keep the existing
opclasses unchanged) or commit 0003 (in which case I'd vote to just stop
supporting the old signature of the consistent function).

The remaining part that didn't get much review is the very last patch,
adding an option to ignore correlation for some BRIN opclases. This is
needed as the regular BRIN costing is quite sensitive to correlation,
and the cost gets way too high for poorly correlated data, making it
unlikely the index will be used. But handling such data sets efficiently
is the main point of those new opclasses. Any opinions on this?

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-introduce-bsearch_arg-20210305.patchtext/x-patch; charset=UTF-8; name=0001-introduce-bsearch_arg-20210305.patchDownload
From 13a2c97a354b32126b18d73af95c1abe63a1c698 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 5 Mar 2021 00:16:40 +0100
Subject: [PATCH 1/8] introduce bsearch_arg

---
 src/backend/statistics/extended_stats.c       | 31 --------------
 src/include/port.h                            |  5 +++
 .../statistics/extended_stats_internal.h      |  5 ---
 src/port/Makefile                             |  1 +
 src/port/bsearch_arg.c                        | 40 +++++++++++++++++++
 5 files changed, 46 insertions(+), 36 deletions(-)
 create mode 100644 src/port/bsearch_arg.c

diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index a030ea3653..fa42851fd5 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -659,37 +659,6 @@ compare_datums_simple(Datum a, Datum b, SortSupport ssup)
 	return ApplySortComparator(a, false, b, false, ssup);
 }
 
-/* simple counterpart to qsort_arg */
-void *
-bsearch_arg(const void *key, const void *base, size_t nmemb, size_t size,
-			int (*compar) (const void *, const void *, void *),
-			void *arg)
-{
-	size_t		l,
-				u,
-				idx;
-	const void *p;
-	int			comparison;
-
-	l = 0;
-	u = nmemb;
-	while (l < u)
-	{
-		idx = (l + u) / 2;
-		p = (void *) (((const char *) base) + (idx * size));
-		comparison = (*compar) (key, p, arg);
-
-		if (comparison < 0)
-			u = idx;
-		else if (comparison > 0)
-			l = idx + 1;
-		else
-			return (void *) p;
-	}
-
-	return NULL;
-}
-
 /*
  * build_attnums_array
  *		Transforms a bitmap into an array of AttrNumber values.
diff --git a/src/include/port.h b/src/include/port.h
index 227ef4b148..82f63de325 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -508,6 +508,11 @@ typedef int (*qsort_arg_comparator) (const void *a, const void *b, void *arg);
 extern void qsort_arg(void *base, size_t nel, size_t elsize,
 					  qsort_arg_comparator cmp, void *arg);
 
+extern void *bsearch_arg(const void *key, const void *base,
+						 size_t nmemb, size_t size,
+						 int (*compar) (const void *, const void *, void *),
+						 void *arg);
+
 /* port/chklocale.c */
 extern int	pg_get_encoding_from_locale(const char *ctype, bool write_message);
 
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index c849bd57c0..a0a3cf5b0f 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -85,11 +85,6 @@ extern int	multi_sort_compare_dims(int start, int end, const SortItem *a,
 extern int	compare_scalars_simple(const void *a, const void *b, void *arg);
 extern int	compare_datums_simple(Datum a, Datum b, SortSupport ssup);
 
-extern void *bsearch_arg(const void *key, const void *base,
-						 size_t nmemb, size_t size,
-						 int (*compar) (const void *, const void *, void *),
-						 void *arg);
-
 extern AttrNumber *build_attnums_array(Bitmapset *attrs, int *numattrs);
 
 extern SortItem *build_sorted_items(int numrows, int *nitems, HeapTuple *rows,
diff --git a/src/port/Makefile b/src/port/Makefile
index e41b005c4f..52dbf5783f 100644
--- a/src/port/Makefile
+++ b/src/port/Makefile
@@ -40,6 +40,7 @@ LIBS += $(PTHREAD_LIBS)
 OBJS = \
 	$(LIBOBJS) \
 	$(PG_CRC32C_OBJS) \
+	bsearch_arg.o \
 	chklocale.o \
 	erand48.o \
 	inet_net_ntop.o \
diff --git a/src/port/bsearch_arg.c b/src/port/bsearch_arg.c
new file mode 100644
index 0000000000..d24dc4b7c4
--- /dev/null
+++ b/src/port/bsearch_arg.c
@@ -0,0 +1,40 @@
+/*
+ *	bsearch_arg.c: bsearch variant with a user-supplied pointer
+ *
+ *	src/port/bsearch_arg.c
+ */
+
+
+#include "c.h"
+
+
+/* simple counterpart to qsort_arg */
+void *
+bsearch_arg(const void *key, const void *base, size_t nmemb, size_t size,
+			int (*compar) (const void *, const void *, void *),
+			void *arg)
+{
+	size_t		l,
+				u,
+				idx;
+	const void *p;
+	int			comparison;
+
+	l = 0;
+	u = nmemb;
+	while (l < u)
+	{
+		idx = (l + u) / 2;
+		p = (void *) (((const char *) base) + (idx * size));
+		comparison = (*compar) (key, p, arg);
+
+		if (comparison < 0)
+			u = idx;
+		else if (comparison > 0)
+			l = idx + 1;
+		else
+			return (void *) p;
+	}
+
+	return NULL;
+}
-- 
2.26.2

0002-Pass-all-scan-keys-to-BRIN-consistent-funct-20210305.patchtext/x-patch; charset=UTF-8; name=0002-Pass-all-scan-keys-to-BRIN-consistent-funct-20210305.patchDownload
From 674079de0e71f183695056b66b2528b4ee154f64 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 2/8] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c          | 158 +++++++++++++++++++-----
 src/backend/access/brin/brin_validate.c |   4 +-
 2 files changed, 126 insertions(+), 36 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 27ba596c6e..963b7079cf 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -390,6 +390,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey   **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -411,6 +414,66 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure. We allocate space for all attributes, so
+	 * that we don't have to bother determining which attributes are used.
+	 *
+	 * XXX The widest table can have ~1600 attributes, so this may allocate a
+	 * couple kilobytes of memory). We could invent a more compact approach
+	 * (with just space for used attributes) but that would make the matching
+	 * more complicated, so it may not be a win.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation used in the
+		 * index column (but only if the search is not IS NULL/ IS NOT NULL).
+		 * Otherwise we shouldn't be using this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno - 1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many scan keys
+			 * are there for this attribute, so we simply allocate the largest
+			 * number possible. This may waste a bit of memory, but we only
+			 * expect small number of scan keys in general, so this should be
+			 * negligible, and it's cheaper than having to repalloc
+			 * repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -471,7 +534,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int			attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -481,51 +544,78 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any san keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of
+					 * multiple scan keys. We can determine that based on the
+					 * number of arguments - functions with extra parameter
+					 * (number of scan keys) do support this, otherwise we
+					 * have to simply pass the scan keys one by one, as
+					 * before.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same
+						 * for all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet
+						 * the criteria for a single one of them is enough to
+						 * discard the range as a whole, so break out of the
+						 * loop as soon as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+
+							/* mismatching key, no need to look further  */
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..11835d85cd 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-- 
2.26.2

0003-Process-all-scan-keys-in-existing-BRIN-opcl-20210305.patchtext/x-patch; charset=UTF-8; name=0003-Process-all-scan-keys-in-existing-BRIN-opcl-20210305.patchDownload
From c264328384014024474f3c18f919c2ea25d816ef Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 5 Mar 2021 00:45:33 +0100
Subject: [PATCH 3/8] Process all scan keys in existing BRIN opclasses

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.
---
 src/backend/access/brin/brin_inclusion.c | 140 ++++++++++++++++-------
 src/backend/access/brin/brin_minmax.c    |  92 ++++++++++++---
 src/include/catalog/pg_proc.dat          |   4 +-
 3 files changed, 177 insertions(+), 59 deletions(-)

diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..a260074c91 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -251,6 +253,10 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 /*
  * BRIN inclusion consistent function
  *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches all those keys, we check the regular scan keys.
+ *
  * All of the strategies are optional.
  */
 Datum
@@ -258,24 +264,31 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/* Handle IS NULL/IS NOT NULL tests */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -284,7 +297,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -293,7 +311,14 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
@@ -301,10 +326,45 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all regular scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
+	/* This should be called only for regular keys, not for IS [NOT] NULL. */
+	Assert(!(key->sk_flags & SK_ISNULL));
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +384,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +444,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..e116084a02 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -140,29 +142,41 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's min/max
  * values.  Return true if so, false otherwise.
+ *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches all those keys, we check the regular scan keys.
  */
 Datum
 brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
 	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -171,7 +185,12 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -180,13 +199,52 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
+	}
+
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
+
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +287,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 59d2b71ca9..93bdb5ec83 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8194,7 +8194,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8210,7 +8210,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0004-Move-IS-NOT-NULL-handling-from-BRIN-support-20210305.patchtext/x-patch; charset=UTF-8; name=0004-Move-IS-NOT-NULL-handling-from-BRIN-support-20210305.patchDownload
From d7084b80b2f73ddca072c5ec320654c561274651 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 2 Mar 2021 19:27:48 +0100
Subject: [PATCH 4/8] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c |  96 +-------
 src/backend/access/brin/brin_minmax.c    |  93 +------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 244 insertions(+), 241 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 963b7079cf..9f2656b8d9 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+								BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -179,7 +182,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -243,31 +245,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -390,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey   **keys;
-	int		   *nkeys;
+	ScanKey   **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -419,13 +399,18 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * consistent support procedure. We allocate space for all attributes, so
 	 * that we don't have to bother determining which attributes are used.
 	 *
+	 * We keep null and regular keys separate, so that we can pass just the
+	 * regular keys to the consistent function easily.
+	 *
 	 * XXX The widest table can have ~1600 attributes, so this may allocate a
 	 * couple kilobytes of memory). We could invent a more compact approach
 	 * (with just space for used attributes) but that would make the matching
 	 * more complicated, so it may not be a win.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -445,23 +430,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno - 1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan keys are
+		 * there for a given attribute, so we simply allocate the largest
+		 * number possible (as if all scan keys belonged to the same
+		 * attribute). This may waste a bit of memory, but we only expect
+		 * small number of scan keys in general, so this should be negligible,
+		 * and it's probably cheaper than having to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many scan keys
-			 * are there for this attribute, so we simply allocate the largest
-			 * number possible. This may waste a bit of memory, but we only
-			 * expect small number of scan keys in general, so this should be
-			 * negligible, and it's cheaper than having to repalloc
-			 * repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -469,9 +454,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -549,15 +548,58 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any san keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular and
+					 * IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine
+					 * mismatch within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all
+					 * passed. If there are no regular scan keys, we're done -
+					 * the page range matches. If there are regular keys, but
+					 * the page range is marked as 'all nulls' it can't
+					 * possibly pass (we're assuming the operators are
+					 * strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -703,7 +745,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -732,25 +773,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1529,6 +1553,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1582,3 +1639,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values; our
+	 * deformed tuple will get updated if the new tuple doesn't fit the
+	 * original range (note this means we can't break out of the loop early).
+	 * Make a note of whether this happens, so that we know to insert the
+	 * modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's the
+			 * first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index a260074c91..b17077703c 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -268,52 +258,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* Handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
 
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
-
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -331,9 +278,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -574,37 +520,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index e116084a02..330bed0487 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -156,52 +146,9 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
 
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -215,9 +162,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -307,34 +253,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 78c89a6961..79440ebe7b 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0005-Optimize-allocations-in-bringetbitmap-20210305.patchtext/x-patch; charset=UTF-8; name=0005-Optimize-allocations-in-bringetbitmap-20210305.patchDownload
From 2daa24a226ff56073f39624664d9f3336ce42209 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 2 Mar 2021 19:57:27 +0100
Subject: [PATCH 5/8] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 60 ++++++++++++++++++++++++++--------
 1 file changed, 47 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 9f2656b8d9..1f82e965f9 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -402,15 +405,52 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * We keep null and regular keys separate, so that we can pass just the
 	 * regular keys to the consistent function easily.
 	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly the
+	 * same lifetime, so that's OK.
+	 *
 	 * XXX The widest table can have ~1600 attributes, so this may allocate a
 	 * couple kilobytes of memory). We could invent a more compact approach
 	 * (with just space for used attributes) but that would make the matching
 	 * more complicated, so it may not be a win.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* regular keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* NULL keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -444,9 +484,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -457,17 +497,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0006-BRIN-bloom-indexes-20210305.patchtext/x-patch; charset=UTF-8; name=0006-BRIN-bloom-indexes-20210305.patchDownload
From 30b65ec54e05acbbbde7cd2e20bfb231df7b2604 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 6/8] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 178 +++++
 doc/src/sgml/ref/create_index.sgml        |  31 +
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 787 ++++++++++++++++++++++
 src/include/access/brin.h                 |   2 +
 src/include/access/brin_internal.h        |   4 +
 src/include/catalog/pg_amop.dat           | 116 ++++
 src/include/catalog/pg_amproc.dat         | 447 ++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 ++
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +-
 src/test/regress/expected/brin_bloom.out  | 428 ++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 376 +++++++++++
 19 files changed, 2534 insertions(+), 6 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 4420794e5b..577dd18c49 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -128,6 +128,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +158,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +172,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +186,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +200,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +214,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +228,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +252,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +266,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +280,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +294,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +308,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +322,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +336,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +350,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +364,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +378,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +392,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +424,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +438,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +452,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +466,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +480,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +494,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +508,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -779,6 +903,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 965dcf472c..a45691873c 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -581,6 +581,37 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default value is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be be
+     greater than 0.0 and smaller than 1.0. The default value is 0.01,
+     which is 1% false positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..4494484b3b
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,787 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test whether it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_page.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001	/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25	/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+
+#define BloomMaxFilterSize \
+	MAXALIGN_DOWN(BLCKSZ - \
+				  (MAXALIGN(SizeOfPageHeaderData + \
+							sizeof(ItemIdData)) + \
+				   MAXALIGN(sizeof(BrinSpecialSpace)) + \
+				   SizeOfBrinTuple))
+
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * To calculate the additional hashes (treating the uint32 hash as an input
+ * value), we use the approach with two hash functions from this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions are calculated using hard-coded seeds.
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes
+ * that are not entirely 0. But while indexes don't support TOAST, the
+ * varlena can still be compressed. So this seems unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high. But even if the false positive rate
+ * exceeds the desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16		flags;
+
+	/* fields for the HASHED phase */
+	uint8		nhashes;		/* number of hash functions */
+	uint32		nbits;			/* number of bits in the bitmap (size) */
+	uint32		nbits_set;		/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+
+}			BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size		len;
+	BloomFilter *filter;
+
+	int			nbits;			/* size of filter / number of bits */
+	int			nbytes;			/* size of filter / number of bytes */
+
+	double		k;				/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	nbits = ceil(-(ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	nbytes = ((nbits + 7) / 8);
+	nbits = nbytes * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * Initially the bloom filter is just zeroes and so very compressible, but
+	 * as we add values it gets more and more random, and so less and less
+	 * compressible. So initially everything fits on the page, but we might
+	 * get surprising failures later - we want to prevent that, so we reject
+	 * bloom filter that are obviously too large.
+	 *
+	 * XXX It's not uncommon to oversize the bloom filter a bit, to defend
+	 * against unexpected data anomalies (parts of table with more distinct
+	 * values per range etc.). But we still need to make sure even the
+	 * oversized filter fits on page, if such need arises.
+	 *
+	 * XXX This check is not perfect, because the index may have multiple
+	 * filters that are small individually, but too large when combined.
+	 */
+	if (nbytes > BloomMaxFilterSize)
+		elog(ERROR, "the bloom filter is too large (%d > %zu)", nbytes,
+			 BloomMaxFilterSize);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * nbits / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so the
+	 * varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + nbytes;
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = nbits;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter * filter, uint32 value, bool *updated)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (!(filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter * filter, uint32 value)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* calculate the two hashes */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (!(filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash), but
+	 * let's keep the array just like inclusion and minman opclasses, for
+	 * consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+}			BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+									uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double		ndistinct;
+	double		maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this case to
+	 * maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a couple of
+	 * safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples, in the
+	 * range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from
+	 * BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to
+				 * return the current page range if the minimum value in the
+				 * range <= scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters for now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing text
+	 * input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 79440ebe7b..8cc4e532e6 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..04d678f96a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,20 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2040,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2062,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2084,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2155,14 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2180,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2202,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2224,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2266,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2288,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2439,20 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2470,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2492,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2548,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2570,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2636,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..6709c8dfea 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..dea9adaf98 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9901',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '9902',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '9903',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9904',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9905',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '9906',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '9907',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '9908',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9909',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9910',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9911',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9912',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9913',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '9914',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '9915',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9916',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9917',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9918',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9919',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 93bdb5ec83..bc38c2a144 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8217,6 +8217,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '9920', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '9921', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '9922', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '9923', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '9924', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11390,4 +11410,18 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8959c2f53b..74e279cbf9 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,5 +679,10 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-
+{ oid => '9925',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..32c56a996a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,428 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..e568b9fea2 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9925 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index c77b0d7342..ecd0806718 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 0264a97324..c0d7fa76f1 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..5d499208e3
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,376 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0007-BRIN-minmax-multi-indexes-20210305.patchtext/x-patch; charset=UTF-8; name=0007-BRIN-minmax-multi-indexes-20210305.patchDownload
From b8bdb3b02373a2c22521d1c195e6654cd063c811 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Wed, 3 Feb 2021 19:00:00 +0100
Subject: [PATCH 7/8] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  252 +-
 doc/src/sgml/ref/create_index.sgml          |   11 +
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 2982 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   33 +-
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    8 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 ++++
 src/include/catalog/pg_amproc.dat           |  600 +++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   85 +
 src/include/catalog/pg_type.dat             |    6 +
 src/test/regress/expected/brin_multi.out    |  445 +++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  397 +++
 21 files changed, 5451 insertions(+), 27 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 577dd18c49..3caa30f104 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -214,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -228,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -242,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -266,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -280,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -294,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -308,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -322,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -336,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -350,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -378,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -392,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -406,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -452,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -466,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -480,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -494,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -508,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -522,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -654,13 +825,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -957,6 +1129,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index a45691873c..e1f0f4c0a0 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -612,6 +612,17 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     eiter a point, or boundary of an interval. Values must be be between
+     8 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..c90c721cac
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2982 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values */
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Sizing the insert buffer - we use 10x the number of values specified
+ * in the reloption, but we cap it to 8192 not to get too large. When
+ * the buffer gets full, we reduce the number of values by half.
+ */
+#define		MINMAX_BUFFER_FACTOR			10
+#define		MINMAX_BUFFER_MIN				256
+#define		MINMAX_BUFFER_MAX				8192
+#define		MINMAX_BUFFER_LOAD_FACTOR		0.5
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+}			MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange; /* number of values per range */
+}			MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0))
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* Cache information that we need quite often. */
+	Oid			typid;
+	Oid			colloid;
+	AttrNumber	attno;
+	FmgrInfo   *cmp;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nsorted;		/* number of sorted values (ranges + points) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/*
+	 * We simply add the values into a large buffer, without any expensive
+	 * steps (sorting, deduplication, ...). The buffer is a multiple of the
+	 * target number of values, so the compaction happen less often,
+	 * amortizing the costs. We keep the actual target and compact to the
+	 * requested number of values at the very end, before serializing to
+	 * on-disk representation.
+	 */
+	/* requested number of values */
+	int			target_maxvalues;
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum		values[FLEXIBLE_ARRAY_MEMBER];
+}			Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, with a simple header
+ * with basic metadata, followed by the boundary values. It has a varlena
+ * header, so can be treated as varlena directly.
+ *
+ * See range_serialize/range_deserialize for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* type of values stored in the data array */
+	Oid			typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+}			SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(int maxvalues, SerializedRanges *range);
+
+
+/*
+ * Used to represent ranges expanded to make merging and combining easier.
+ *
+ * Each expanded range is essentially an interval, represented by min/max
+ * values, along with a flag whether it's a collapsed range (in which case
+ * the min and max values are equal). We have the flag to handle by-ref
+ * data types - we can't simply compare the datums, and this saves some
+ * calls to the type-specific comparator function.
+ */
+typedef struct ExpandedRange
+{
+	Datum		minval;			/* lower boundary */
+	Datum		maxval;			/* upper boundary */
+	bool		collapsed;		/* true if minval==maxval */
+}			ExpandedRange;
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of extended ranges).
+ */
+typedef struct DistanceValue
+{
+	int			index;
+	double		value;
+}			DistanceValue;
+
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+										   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+													uint16 attno, Oid subtype,
+													uint16 strategynum);
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+}			compare_context;
+
+static int	compare_values(const void *a, const void *b, void *arg);
+
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+	int			i;
+	Datum		lt;
+
+	for (i = 0; i < (nvalues - 1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i + 1]);
+		Assert(DatumGetBool(lt));
+	}
+}
+#endif
+
+/*
+ * Comprehensive check of the Ranges structure.
+ */
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+
+	/* some basic sanity checks */
+	Assert(ranges->nranges >= 0);
+	Assert(ranges->nsorted >= 0);
+	Assert(ranges->nvalues >= ranges->nsorted);
+	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
+	Assert(ranges->typid != InvalidOid);
+
+	/*
+	 * First the ranges - there are 2*nranges boundary values, and the values
+	 * have to be strictly ordered (equal values would mean the range is
+	 * collapsed, and should be stored as a point). This also guarantees that
+	 * the ranges do not overlap.
+	 */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2 * ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2 * ranges->nranges],
+					 ranges->nsorted);
+
+	/*
+	 * Check that none of the values are not covered by ranges (both sorted
+	 * and unsorted)
+	 */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		compar;
+		int			start,
+					end;
+		Datum		minvalue,
+					maxvalue;
+
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nranges == 0)
+			break;
+
+		minvalue = ranges->values[0];
+		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+		/* smaller than the smallest value in the first range */
+		if (DatumGetBool(compar))
+			continue;
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+		/* larger than the largest value in the last range */
+		if (DatumGetBool(compar))
+			continue;
+
+		start = 0;				/* first range */
+		end = ranges->nranges - 1;	/* last range */
+		while (true)
+		{
+			int			midpoint = (start + end) / 2;
+
+			/* this means we ran out of ranges in the last step */
+			if (start > end)
+				break;
+
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2 * midpoint];
+			maxvalue = ranges->values[2 * midpoint + 1];
+
+			/*
+			 * Is the value smaller than the minval? If yes, we'll recurse to
+			 * the left side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+			{
+				end = (midpoint - 1);
+				continue;
+			}
+
+			/*
+			 * Is the value greater than the minval? If yes, we'll recurse to
+			 * the right side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+			{
+				start = (midpoint + 1);
+				continue;
+			}
+
+			/* hey, we found a matching range */
+			Assert(false);
+		}
+	}
+
+	/* and values in the unsorted part must not be in sorted part */
+	for (i = ranges->nsorted; i < ranges->nvalues; i++)
+	{
+		compare_context cxt;
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nsorted == 0)
+			break;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		Assert(bsearch_arg(&value, &ranges->values[2 * ranges->nranges],
+						   ranges->nsorted, sizeof(Datum),
+						   compare_values, (void *) &cxt) == NULL);
+	}
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidExpandedRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						  Form_pg_attribute attr, ExpandedRange *ranges,
+						  int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+	FmgrInfo   *eq;
+	FmgrInfo   *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum		r;
+		Datum		minval = ranges[i].minval;
+		Datum		maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else					/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e. upper <
+	 * lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges - 1; i++)
+	{
+		Datum		r;
+		Datum		maxval = ranges[i].maxval;
+		Datum		minval = ranges[i + 1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size		len;
+	Ranges	   *ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values); /* fixed header */
+	len += maxvalues * sizeof(Datum);	/* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+
+/*
+ * range_deduplicate_values
+ *		Deduplicate the part with values in the simple points.
+ *
+ * This is meant to be a cheaper way of reducing the size of the ranges. It
+ * does not touch the ranges, and only sorts the other values - it does not
+ * call the distance functions, which may be quite expensive, etc.
+ */
+static void
+range_deduplicate_values(Ranges *range)
+{
+	int			i,
+				n;
+	int			start;
+	compare_context cxt;
+
+	/*
+	 * If there are no unsorted values, we're done (this probably can't
+	 * happen, as we're adding values to unsorted part).
+	 */
+	if (range->nsorted == range->nvalues)
+		return;
+
+	/* sort the values */
+	cxt.colloid = range->colloid;
+	cxt.cmpFn = range->cmp;
+
+	/* how many values to sort? */
+	start = 2 * range->nranges;
+
+	qsort_arg(&range->values[start],
+			  range->nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	n = 1;
+	for (i = 1; i < range->nvalues; i++)
+	{
+		/* same as preceding value, so store it */
+		if (compare_values(&range->values[start + i - 1],
+						   &range->values[start + i],
+						   (void *) &cxt) == 0)
+			continue;
+
+		range->values[start + n] = range->values[start + i];
+
+		n++;
+	}
+
+	/* now all the values are sorted */
+	range->nvalues = n;
+	range->nsorted = n;
+
+	AssertCheckRanges(range, range->cmp, range->colloid);
+}
+
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size		len;
+	int			nvalues;
+	SerializedRanges *serialized;
+	Oid			typid;
+	int			typlen;
+	bool		typbyval;
+
+	int			i;
+	char	   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nsorted >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+	Assert(range->target_maxvalues > 0);
+
+	/* at this point the range should be compacted to the target size */
+	Assert(2 * range->nranges + range->nvalues <= range->target_maxvalues);
+
+	Assert(range->target_maxvalues <= range->maxvalues);
+
+	/* range boundaries are always sorted */
+	Assert(range->nvalues >= range->nsorted);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2 * range->nranges + range->nvalues;
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges, data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)			/* varlena */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)		/* cstring */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else						/* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->target_maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation this
+	 * depends on the particular data type).
+	 */
+	ptr = serialized->data;		/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int			tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int			tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + len));
+	}
+
+	/* exact size */
+	Assert(ptr == ((char *) serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(int maxvalues, SerializedRanges *serialized)
+{
+	int			i,
+				nvalues;
+	char	   *ptr;
+	bool		typbyval;
+	int			typlen;
+
+	Ranges	   *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2 * serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+	Assert(serialized->maxvalues <= maxvalues);
+
+	range = minmax_multi_init(maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->nsorted = serialized->nvalues;
+	range->maxvalues = maxvalues;
+	range->target_maxvalues = serialized->maxvalues;
+
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need to copy
+	 * the values and will instead just point the values to the serialized
+	 * varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *) serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+/*
+ * compare_expanded_ranges
+ *	  Compare the expanded ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_expanded_ranges(const void *a, const void *b, void *arg)
+{
+	ExpandedRange *ra = (ExpandedRange *) a;
+	ExpandedRange *rb = (ExpandedRange *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * compare_values
+ *	  Compare the values.
+ */
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum	   *da = (Datum *) a;
+	Datum	   *db = (Datum *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *da, *db);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *db, *da);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Check if the new value matches one of the existing ranges.
+ */
+static bool
+has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges,
+				   Datum newval, AttrNumber attno, Oid typid)
+{
+	Datum		compar;
+
+	Datum		minvalue = ranges->values[0];
+	Datum		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+
+	/* binary search on ranges */
+	int			start,
+				end;
+
+	if (ranges->nranges == 0)
+		return false;
+
+	/*
+	 * Otherwise, need to compare the new value with boundaries of all the
+	 * ranges. First check if it's less than the absolute minimum, which is
+	 * the first value in the array.
+	 */
+	cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												   BTLessStrategyNumber);
+	compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+	/* smaller than the smallest value in the range list */
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * And now compare it to the existing maximum (last value in the data
+	 * array). But only if we haven't already ruled out a possible match in
+	 * the minvalue check.
+	 */
+	cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													  BTGreaterStrategyNumber);
+	compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * So we know it's in the general min/max, the question is whether it
+	 * falls in one of the ranges or gaps. We'll do a binary search on the on
+	 * the individual ranges - for each range we check equality (value falls
+	 * into the range), and then check ranges either above or below the
+	 * current range.
+	 */
+	start = 0;					/* first range */
+	end = (ranges->nranges - 1);	/* last range */
+	while (true)
+	{
+		int			midpoint = (start + end) / 2;
+
+		/* this means we ran out of ranges in the last step */
+		if (start > end)
+			return false;
+
+		/* copy the min/max values from the ranges */
+		minvalue = ranges->values[2 * midpoint];
+		maxvalue = ranges->values[2 * midpoint + 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in this range */
+		if (DatumGetBool(compar))
+		{
+			end = (midpoint - 1);
+			continue;
+		}
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+		/* larger than the largest value in this range */
+		if (DatumGetBool(compar))
+		{
+			start = (midpoint + 1);
+			continue;
+		}
+
+		/* hey, we found a matching range */
+		return true;
+	}
+
+	return false;
+}
+
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX We only inspect the sorted parts, which means we may add duplicate
+ * values. It may produce some false negatives when adding the values, but
+ * only after we already added some values (otherwise there is no unsorted
+ * part). And when querying the index, there should be no unsorted values,
+ * because the values are sorted and deduplicated during serialization.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+					 AttrNumber attno, Form_pg_attribute attr,
+					 Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid))
+		return true;
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													BTEqualStrategyNumber);
+
+	/*
+	 * There is no matching range, so let's inspect the sorted values.
+	 *
+	 * XXX We do a sequential search for small number of values, and binary
+	 * search once we have more than 16 values. This threshold is somewhat
+	 * arbitrary, as it depends on how expensive the comparison function is.
+	 * So maybe we should just do the binary search all the time.
+	 *
+	 * XXX If we use the threshold here, maybe we should do the same thing in
+	 * has_matching_range?
+	 */
+	if (ranges->nsorted >= 16)
+	{
+		compare_context cxt;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		if (bsearch_arg(&newval, &ranges->values[2 * ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) != NULL)
+			return true;
+	}
+	else
+	{
+		for (i = 2 * ranges->nranges; i < 2 * ranges->nranges + ranges->nsorted; i++)
+		{
+			Datum		compar;
+
+			compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+			/* found an exact match */
+			if (DatumGetBool(compar))
+				return true;
+		}
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * Expand ranges from Ranges into ExpandedRange array. This expects the
+ * eranges to be pre-allocated and with the correct size - there needs to be
+ * (nranges + nvalues) elements.
+ *
+ * The order of expanded ranges is arbitrary. We do expand the ranges first,
+ * and this part is sorted. But then we expand the values, and this part may
+ * be unsorted.
+ */
+static void
+fill_expanded_ranges(ExpandedRange *eranges, int neranges, Ranges *ranges)
+{
+	int			idx;
+	int			i;
+
+	/* Check that the output array has the right size. */
+	Assert(neranges == (2 * ranges->nranges + ranges->nvalues));
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * i];
+		eranges[idx].maxval = ranges->values[2 * i + 1];
+		eranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].maxval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	/* Did we produce the expected number of elements? */
+	Assert(idx == neranges);
+
+	return;
+}
+
+/*
+ * Sort and deduplicate expanded ranges.
+ *
+ * The ranges may be deduplicated - we're simply appending values, without
+ * checking for duplicates etc. So maybe the deduplication will reduce the
+ * number of ranges enough, and we won't have to compute the distances etc.
+ *
+ * Returns the number of expanded ranges.
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that some of the input data is already sorted (all the ranges
+ * and possibly some of the points) and do merge sort.
+ */
+static int
+sort_expanded_ranges(FmgrInfo *cmp, Oid colloid,
+					 ExpandedRange *eranges, int neranges)
+{
+	int			n;
+	int			i;
+	compare_context cxt;
+
+	Assert(neranges > 0);
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(eranges, neranges, sizeof(ExpandedRange),
+			  compare_expanded_ranges, (void *) &cxt);
+
+	/*
+	 * Deduplicate the ranges - simply compare each range to the preceding
+	 * one, and skip the duplicate ones.
+	 */
+	n = 1;
+	for (i = 1; i < neranges; i++)
+	{
+		/* if the current range is equal to the preceding one, do nothing */
+		if (!compare_expanded_ranges(&eranges[i - 1], &eranges[i], (void *) &cxt))
+			continue;
+
+		/* otherwise copy it to n-th place (if not already there) */
+		if (i != n)
+			memcpy(&eranges[n], &eranges[i], sizeof(ExpandedRange));
+
+		n++;
+	}
+
+	Assert((n > 0) && (n <= neranges));
+
+	return n;
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the expanded ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_overlapping_ranges(FmgrInfo *cmp, Oid colloid,
+						 ExpandedRange *eranges, int neranges)
+{
+	int			idx;
+
+	/* Merge ranges (idx) and (idx+1) if they overlap. */
+	idx = 0;
+	while (idx < (neranges - 1))
+	{
+		Datum		r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap if (minval
+		 * < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges are
+		 * ordered, so there are no more overlaps, because all the remaining
+		 * ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].maxval);
+
+		if (DatumGetBool(r))
+			eranges[idx].maxval = eranges[idx + 1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of the
+		 * previous state).
+		 */
+		eranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the remaining
+		 * ranges by 1. There are ncranges elements, and we need to move
+		 * elements from (idx+2). That means the number of elements to move is
+		 * [ncranges - (idx+2)].
+		 */
+		memmove(&eranges[idx + 1], &eranges[idx + 2],
+				(neranges - (idx + 2)) * sizeof(ExpandedRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range, as
+		 * it might overlap with additional ranges thanks to the merge).
+		 */
+		neranges--;
+	}
+
+	return neranges;
+}
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ * This is intentionally sorting the distances in descending order, i.e.
+ * the longer gaps will be at the front.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *) a;
+	DistanceValue *db = (DistanceValue *) b;
+
+	if (da->value < db->value)
+		return 1;
+	else if (da->value > db->value)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Given an array of expanded ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (max-min) for pairs
+ * of consecutive ranges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_expanded_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			ndistances;
+	DistanceValue *distances;
+
+	Assert(neranges >= 2);
+
+	ndistances = (neranges - 1);
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * ndistances);
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges so
+	 * that we can sort them once.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		Datum		a1,
+					a2,
+					r;
+
+		a1 = eranges[i].maxval;
+		a2 = eranges[i + 1].minval;
+
+		/* compute length of the gap (between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index of the gap the distance is for */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/*
+	 * Sort the distances in descending order, so that the longest gaps are at
+	 * the front.
+	 */
+	pg_qsort(distances, ndistances, sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds expanded ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).  This expanded
+ * representation makes the processing a bit easier, as it allows handling
+ * ranges and points the same way.
+ *
+ * We sort and deduplicate the expanded ranges - this is necessary, because
+ * the points may be unsorted. And moreover the two parts (ranges and
+ * points) are sorted on their own.
+ */
+static ExpandedRange *
+build_expanded_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					  int *nranges)
+{
+	int			neranges;
+	ExpandedRange *eranges;
+
+	/* both ranges and points are expanded into a separate element */
+	neranges = ranges->nranges + ranges->nvalues;
+
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges */
+	fill_expanded_ranges(eranges, neranges, ranges);
+
+	/* sort and deduplicate the expanded ranges */
+	neranges = sort_expanded_ranges(cmp, colloid, eranges, neranges);
+
+	/* remember how many cranges we built */
+	*nranges = neranges;
+
+	return eranges;
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(ExpandedRange *cranges, int ncranges)
+{
+	int			i;
+	int			count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+#endif
+
+/*
+ * reduce_expanded_ranges
+ *		reduce the ranges until the number of values is low enough
+ *
+ * Combines ranges until the number of boundary values drops below the
+ * threshold specified by max_values. This happens by merging enough
+ * ranges by distance between them.
+ *
+ * Returns the number of result ranges.
+ *
+ * We simply use the global min/max and then add boundaries for enough
+ * largest gaps. Each gap adds 2 values, so we simply use (target/2-1)
+ * distances. Then we simply sort all the values - each two values are
+ * a boundary of a range (possibly collapsed).
+ *
+ * XXX Some of the ranges may be collapsed (i.e. the min/max values are
+ * equal), but we ignore that for now. We could repeat the process,
+ * adding a couple more gaps recursively.
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ *
+ * XXX Or maybe we could just handle this by using random value as a
+ * tie-break, or by adding random noise to the actual distance.
+ */
+static int
+reduce_expanded_ranges(ExpandedRange *eranges, int neranges,
+					   DistanceValue *distances, int max_values,
+					   FmgrInfo *cmp, Oid colloid)
+{
+	int			i;
+	int			nvalues;
+	Datum	   *values;
+
+	compare_context cxt;
+
+	/* total number of gaps between ranges */
+	int			ndistances = (neranges - 1);
+
+	/* number of gaps to keep */
+	int			keep = (max_values / 2 - 1);
+
+	/*
+	 * Maybe we have sufficiently low number of ranges already?
+	 *
+	 * XXX This should happen before we actually do the expensive stuff like
+	 * sorting, so maybe this should be just an assert.
+	 */
+	if (keep >= ndistances)
+		return neranges;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	/* add the global min/max values, from the first/last range */
+	values[nvalues++] = eranges[0].minval;
+	values[nvalues++] = eranges[neranges - 1].maxval;
+
+	/* add boundary values for enough gaps */
+	for (i = 0; i < keep; i++)
+	{
+		/* index of the gap between (index) and (index+1) ranges */
+		int			index = distances[i].index;
+
+		Assert((index >= 0) && ((index + 1) < neranges));
+
+		/* add max from the preceding range, minval from the next one */
+		values[nvalues++] = eranges[index].maxval;
+		values[nvalues++] = eranges[index + 1].minval;
+
+		Assert(nvalues <= max_values);
+	}
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/*
+	 * Sort the values using the comparator function, and form ranges from the
+	 * sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		eranges[i].minval = values[2 * i];
+		eranges[i].maxval = values[2 * i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		eranges[i].collapsed = (compare_values(&values[2 * i],
+											   &values[2 * i + 1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+/*
+ * Store the boundary values from ExpandedRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_expanded_ranges(Ranges *ranges, ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (!eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->values[idx++] = eranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+
+	/* all the values are sorted */
+	ranges->nsorted = ranges->nvalues;
+
+	Assert(count_values(eranges, neranges) == 2 * ranges->nranges + ranges->nvalues);
+	Assert(2 * ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+}
+
+
+/*
+ * Consider freeing space in the ranges.
+ *
+ * Returns true if the value was actually modified.
+ */
+static bool
+ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *range)
+{
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	/*
+	 * If there is free space in the buffer, we're done without having to
+	 * modify anything.
+	 */
+	if (2 * range->nranges + range->nvalues < range->maxvalues)
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* did we reduce enough free space by just the deduplication? */
+	if (2 * range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR)
+		return true;
+
+	/*
+	 * We need to combine some of the existing ranges, to reduce the number of
+	 * values we have to store.
+	 *
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it (we might have
+	 * to do this repeatedly, even for a single BRIN page range). Otherwise
+	 * we'd have problems e.g. when building new indexes. So we use a memory
+	 * context and make sure we free the memory at the end (so if we call the
+	 * distance function many times, it might be an issue, but meh).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, colloid, range, &neranges);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not use
+	 * too low or high value.
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR,
+									  cmpFn, colloid);
+
+	/* Make sure we've sufficiently reduced the number of ranges. */
+	Assert(count_values(eranges, neranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR);
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(range, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Did we break the ranges somehow? */
+	AssertCheckRanges(range, cmpFn, colloid);
+
+	return true;
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn;
+	bool		modified = false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Make sure there's enough free space in the buffer. We only trigger this
+	 * when the buffer is full, which means it had to be modified as we size
+	 * it to be larger than what is stored on disk.
+	 *
+	 * XXX This needs to happen before we check if the value is contained in
+	 * the range, because the value might be in the unsorted part, and we
+	 * don't check that in range_contains_value. The deduplication would then
+	 * move it to the sorted part, and we'd add the value too, which violates
+	 * the rule that we never have duplicates with the ranges or sorted
+	 * values.
+	 *
+	 * XXX At the moment this only does the deduplication.
+	 *
+	 * XXX We might also deduplicate and recheck if the value is contained,
+	 * but that seems like an overkill. We'd need to deduplicate anyway, so
+	 * why not do it now.
+	 */
+	modified = ensure_free_space_in_buffer(bdesc, colloid,
+										   attno, attr, ranges);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then do the
+	 * deduplication in a batch, hoping for better efficiency. But that would
+	 * mean we actually modify the range every time, which means having to
+	 * serialize the value, which does palloc, walks the values, copies them,
+	 * etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming the
+	 * comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return modified;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we do a
+	 * simple insertion sort. We could do something more elaborate, e.g. by
+	 * sorting the values only now and then, but for small counts (e.g. when
+	 * maxvalues is 64) this should be fine.
+	 */
+	ranges->values[2 * ranges->nranges + ranges->nvalues] = newval;
+	ranges->nvalues++;
+
+	/*
+	 * Check we haven't broken the ordering of boundary values (checks both
+	 * parts, but that doesn't hurt).
+	 */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/* Also check the range contains the value we just added. */
+	/* FIXME Assert(ranges, cmpFn, colloid); */
+
+	/* yep, we've modified the range */
+	return true;
+}
+
+/*
+ * Generate range representation of data collected during "batch mode".
+ * This is similar to reduce_expanded_ranges, except that we can't assume
+ * the values are sorted and there may be duplicate values.
+ */
+static void
+compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
+											   BTLessStrategyNumber);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, ranges->attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, ranges->colloid, ranges, &neranges);
+
+	/* FIXME this should do neranges >= max_values */
+	if (neranges > 1)
+	{
+		/* build array of gap distances and sort them in ascending order */
+		distances = build_distances(distanceFn, ranges->colloid,
+									eranges, neranges);
+
+		/*
+		 * Combine ranges until we get below max_values. We don't use any
+		 * scale factor, because this is used during serialization, and we
+		 * don't expect more tuples to be inserted anytime soon.
+		 */
+		neranges = reduce_expanded_ranges(eranges, neranges, distances,
+										  max_values, cmpFn, ranges->colloid);
+
+		Assert(count_values(eranges, neranges) <= max_values);
+	}
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(ranges, eranges, neranges);
+
+	/* check all the range invariants */
+	AssertCheckRanges(ranges, cmpFn, ranges->colloid);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float		a1 = PG_GETARG_FLOAT4(0);
+	float		a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double		a1 = PG_GETARG_FLOAT8(0);
+	double		a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16		a1 = PG_GETARG_INT16(0);
+	int16		a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32		a1 = PG_GETARG_INT32(0);
+	int32		a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64		a1 = PG_GETARG_INT64(0);
+	int64		a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double		da1,
+				da2;
+
+	ItemPointer pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	/*
+	 * We use the no-check variants here, because user-supplied values may
+	 * have (ip_posid == 0). See ItemPointerCompare.
+	 */
+	da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa1);
+
+	da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum		d;
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int			i;
+	float8		delta = 0;
+
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN - 1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute approximate distance between two dates.
+ */
+Datum
+brin_minmax_multi_distance_date(PG_FUNCTION_ARGS)
+{
+	DateADT		dateVal1 = PG_GETARG_DATEADT(0);
+	DateADT		dateVal2 = PG_GETARG_DATEADT(1);
+
+	if (DATE_NOT_FINITE(dateVal1) || DATE_NOT_FINITE(dateVal2))
+		PG_RETURN_FLOAT8(0);
+
+	PG_RETURN_FLOAT8(dateVal1 - dateVal2);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeADT		ta = PG_GETARG_TIMEADT(0);
+	TimeADT		tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+Datum
+brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Timestamp	dt1 = PG_GETARG_TIMESTAMP(0);
+	Timestamp	dt2 = PG_GETARG_TIMESTAMP(1);
+
+	if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
+		PG_RETURN_FLOAT8(0);
+
+	delta = dt2 - dt1;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Interval   *ia = PG_GETARG_INTERVAL_P(0);
+	Interval   *ib = PG_GETARG_INTERVAL_P(1);
+	Interval   *result;
+
+	result = (Interval *) palloc(sizeof(Interval));
+
+	result->month = ib->month - ia->month;
+	/* overflow check copied from int4mi */
+	if (!SAMESIGN(ib->month, ia->month) &&
+		!SAMESIGN(result->month, ib->month))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->day = ib->day - ia->day;
+	if (!SAMESIGN(ib->day, ia->day) &&
+		!SAMESIGN(result->day, ib->day))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->time = ib->time - ia->time;
+	if (!SAMESIGN(ib->time, ia->time) &&
+		!SAMESIGN(result->time, ib->time))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	/*
+	 * We assume months have 31 days - we don't need to be precise, in the
+	 * worst case we'll build somewhat less efficient ranges.
+	 */
+	delta = (float8) (result->month * 31 + result->day);
+
+	/* convert to microseconds (just like the time part) */
+	delta = 24L * 3600L * delta;
+
+	/* and add the time part */
+	delta += result->time / (float8) 1000000.0;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((float8) b->h - (float8) a->h);
+	delta /= 256;
+
+	delta += ((float8) b->g - (float8) a->g);
+	delta /= 256;
+
+	delta += ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+	int			i;
+	int			len;
+	unsigned char *addra,
+			   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be in
+	 * maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		PG_RETURN_FLOAT8(1.0);
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16;				/* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len - 1; i >= 0; i--)
+	{
+		delta += (float8) addrb[i] - (float8) addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
+{
+	Ranges	   *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s;
+
+	/*
+	 * In batch mode, we need to compress the accumulated values to the
+	 * actually requested number of values/ranges.
+	 */
+	compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+
+	s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges	   *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, is possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 *
+	 * When starting with an empty range, we assume this is a batch mode, i.e.
+	 * we size the buffer for the maximum possible number of items in the
+	 * range (based on range size and max number of items on a page).
+	 *
+	 * XXX This may require quite a bit of memory, so maybe we should use some
+	 * value in between. OTOH most tables will have much wider rows, so the
+	 * number of rows per page is much lower.
+	 *
+	 * XXX Maybe we should do this (using larger buffer) even when there
+	 * already is a summary?
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		int			target_maxvalues;
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		/* what was specified as a reloption? */
+		target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, target_maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+		ranges = minmax_multi_init(maxvalues);
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+		ranges->target_maxvalues = target_maxvalues;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, serialized->maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		ranges = range_deserialize(maxvalues, serialized);
+
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+
+	/* MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS(); */
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges	   *ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized->maxvalues, serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum		minval = ranges->values[2 * rangeno];
+		Datum		maxval = ranges->values[2 * rangeno + 1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+					{
+						Datum		compar;
+						FmgrInfo   *cmpFn;
+
+						/* by default this range does not match */
+						matches = false;
+
+						/*
+						 * Otherwise, need to compare the new value with
+						 * boundaries of all the ranges. First check if it's
+						 * less than the absolute minimum, which is the first
+						 * value in the array.
+						 */
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTLessStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+						/* smaller than the smallest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTGreaterStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+						/* larger than the largest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						/*
+						 * haven't managed to eliminate this range, so
+						 * consider it matching
+						 */
+						matches = true;
+
+						break;
+					}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		val = ranges->values[2 * ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+
+	/* MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS(); */
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges *serialized_a;
+	SerializedRanges *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	ExpandedRange *eranges;
+	int			neranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue *distances;
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a);
+	ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	neranges = (ranges_a->nranges + ranges_a->nvalues) +
+		(ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges with entries for the first range */
+	fill_expanded_ranges(eranges, ranges_a->nranges + ranges_a->nvalues,
+						 ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_expanded_ranges(&eranges[ranges_a->nranges + ranges_a->nvalues],
+						 ranges_b->nranges + ranges_b->nvalues,
+						 ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* sort the expanded ranges */
+	sort_expanded_ranges(cmpFn, colloid, eranges, neranges);
+
+	/*
+	 * We've loaded two different lists of expanded ranges, so some of them
+	 * may be overlapping. So walk through them and merge them.
+	 */
+	neranges = merge_overlapping_ranges(cmpFn, colloid, eranges, neranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidExpandedRanges(bdesc, colloid, attno, attr, eranges, neranges);
+
+	/*
+	 * If needed, reduce some of the ranges.
+	 *
+	 * XXX This may be fairly expensive, so maybe we should do it only when
+	 * it's actually needed (when we have too many ranges).
+	 */
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the threshold. The
+	 * collapsed ranges will be stored as a single value.
+	 *
+	 * XXX This does not apply the load factor, as we don't expect to add more
+	 * values to the range, so we prefer to keep as many ranges as possible.
+	 *
+	 * XXX Can the maxvalues be different in the two ranges? Perhaps we should
+	 * use maximum of those?
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  ranges_a->maxvalues,
+									  cmpFn, colloid);
+
+	/* update the first range summary */
+	store_expanded_ranges(ranges_a, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+								   uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 8, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges	   *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges->maxvalues, ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum		a,
+					b;
+		text	   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum		a;
+		text	   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..69ce6dfca4 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(brdesc,
+												  tuple->bt_columns[keyno].bv_mem_value,
+												  tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -169,15 +177,15 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
 		{
-			Datum value = tuple->bt_columns[keyno].bv_values[datumno];
+			Datum		value = tuple->bt_columns[keyno].bv_values[datumno];
 
 #ifdef TOAST_INDEX_HACK
 
 			/* We must look at the stored type, not at the index descriptor. */
-			TypeCacheEntry	*atttype = brdesc->bd_info[keyno]->oi_typcache[datumno];
+			TypeCacheEntry *atttype = brdesc->bd_info[keyno]->oi_typcache[datumno];
 
 			/* Do we need to free the value at the end? */
-			bool free_value = false;
+			bool		free_value = false;
 
 			/* For non-varlena types we don't need to do anything special */
 			if (atttype->typlen != -1)
@@ -193,9 +201,9 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 * If value is stored EXTERNAL, must fetch it so we are not
 			 * depending on outside storage.
 			 *
-			 * XXX Is this actually true? Could it be that the summary is
-			 * NULL even for range with non-NULL data? E.g. degenerate bloom
-			 * filter may be thrown away, etc.
+			 * XXX Is this actually true? Could it be that the summary is NULL
+			 * even for range with non-NULL data? E.g. degenerate bloom filter
+			 * may be thrown away, etc.
 			 */
 			if (VARATT_IS_EXTERNAL(DatumGetPointer(value)))
 			{
@@ -205,8 +213,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			}
 
 			/*
-			 * If value is above size target, and is of a compressible datatype,
-			 * try to compress it in-line.
+			 * If value is above size target, and is of a compressible
+			 * datatype, try to compress it in-line.
 			 */
 			if (!VARATT_IS_EXTENDED(DatumGetPointer(value)) &&
 				VARSIZE(DatumGetPointer(value)) > TOAST_INDEX_TARGET &&
@@ -495,6 +503,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +587,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 8cc4e532e6..fdaff42722 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for multi-minmax indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..87de94f397 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,11 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+/*
+ * The BRIN opclasses may register serialization callback, in case the on-disk
+ * and in-memory representations differ (e.g. for performance reasons).
+ */
+typedef void (*brin_serialize_callback_type) (BrinDesc *bdesc, Datum src, Datum *dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +32,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 82e874130d..654584a03f 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 04d678f96a..8135854163 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2062,6 +2208,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2088,6 +2251,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2155,6 +2334,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2180,6 +2425,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2202,6 +2464,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2224,6 +2503,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2288,6 +2584,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2439,6 +2752,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2470,6 +2929,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2492,6 +2968,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2548,6 +3041,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2570,6 +3080,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2636,6 +3163,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 6709c8dfea..51403716b1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index dea9adaf98..ba9231ac8c 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9926',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '9901',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9927',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '9902',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9928',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '9904',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9929',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '9905',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9930',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '9909',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '9910',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9931',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9932',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '9911',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9933',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '9912',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9934',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '9913',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9935',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '9914',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9936',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '9916',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9937',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '9917',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9938',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '9918',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9939',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '9919',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index bc38c2a144..3035b3b543 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8200,6 +8200,77 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9940', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9941', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9942', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9943', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9944', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9945', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9946', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9947', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9948', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9949', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9950', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9951', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9952', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9953', descr => 'BRIN multi minmax date distance',
+  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
+{ oid => '9954', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9955', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9956', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9957', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9958', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9959', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9960', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+{ oid => '9961', descr => 'BRIN multi minmax timestamp distance',
+  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11424,4 +11495,18 @@
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
 
+{ oid => '9962', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9963', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9964', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9965', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 74e279cbf9..e809094490 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -685,4 +685,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+{ oid => '9966',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..e13cb59c7e
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,445 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+ERROR:  value 7 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+DROP INDEX brinidx_multi;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index e568b9fea2..0541c12a25 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9925 | pg_brin_bloom_summary
-(5 rows)
+ 9966 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ecd0806718..d34fc7ef88 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index c0d7fa76f1..91834a984e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..6d61fb84c6
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,397 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+
+DROP INDEX brinidx_multi;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0008-Ignore-correlation-for-new-BRIN-opclasses-20210305.patchtext/x-patch; charset=UTF-8; name=0008-Ignore-correlation-for-new-BRIN-opclasses-20210305.patchDownload
From b4d19a0f5a4425a96aa2728e3fbeac3f52da0de1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 8/8] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 4494484b3b..46ffc068be 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -412,6 +412,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index c90c721cac..a6f79311e2 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1737,6 +1737,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 52314d3aa1..0320d128f6 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index fdaff42722..5fbf8cf9c7 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

#156Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#155)
8 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

Here is a slightly improved patch series, fixing a lot of wording issues
and typos (thanks to Justin Pryzby). I also realized create_index.sgml
is not the right place to document opclass parameters - that worked when
the parameters were speficified in WITH, but that's no longer the case.

So I've moved this bit to brin.sgml, under the table listing opclasses.

On 3/5/21 1:37 AM, Tomas Vondra wrote:

Hi,

Here is an updated version of the patch series, with a couple minor
changes/improvements.

1) adding bsearch_arg to src/port/

2) moving minmax/inclusion changes from 0002 to a separate patch 0003

I think we should either ditch the 0003 (i.e. keep the existing
opclasses unchanged) or commit 0003 (in which case I'd vote to just stop
supporting the old signature of the consistent function).

Still not sure what do to about this. I'm leaning towards keeping 0003
and just removing the "old" signature entirely, to keep the API cleaner.
It might cause some breakage in out-of-core BRIN opclasses, but that
seems like a reasonable price. Moreover, the opclasses may need some
updating anyway, because of the changes in handling NULL scan keys (0004
moves that from the opclass to the bringetbitmap function).

The remaining part that didn't get much review is the very last patch,
adding an option to ignore correlation for some BRIN opclases. This is
needed as the regular BRIN costing is quite sensitive to correlation,
and the cost gets way too high for poorly correlated data, making it
unlikely the index will be used. But handling such data sets efficiently
is the main point of those new opclasses. Any opinions on this?

Not sure about this.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-introduce-bsearch_arg-20210308.patchtext/x-patch; charset=UTF-8; name=0001-introduce-bsearch_arg-20210308.patchDownload
From 2975a9f12317bdf12e4ef356944202130fdf7bdc Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 5 Mar 2021 00:16:40 +0100
Subject: [PATCH 1/8] introduce bsearch_arg

---
 src/backend/statistics/extended_stats.c       | 31 --------------
 src/include/port.h                            |  5 +++
 .../statistics/extended_stats_internal.h      |  5 ---
 src/port/Makefile                             |  1 +
 src/port/bsearch_arg.c                        | 40 +++++++++++++++++++
 5 files changed, 46 insertions(+), 36 deletions(-)
 create mode 100644 src/port/bsearch_arg.c

diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index a030ea3653..fa42851fd5 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -659,37 +659,6 @@ compare_datums_simple(Datum a, Datum b, SortSupport ssup)
 	return ApplySortComparator(a, false, b, false, ssup);
 }
 
-/* simple counterpart to qsort_arg */
-void *
-bsearch_arg(const void *key, const void *base, size_t nmemb, size_t size,
-			int (*compar) (const void *, const void *, void *),
-			void *arg)
-{
-	size_t		l,
-				u,
-				idx;
-	const void *p;
-	int			comparison;
-
-	l = 0;
-	u = nmemb;
-	while (l < u)
-	{
-		idx = (l + u) / 2;
-		p = (void *) (((const char *) base) + (idx * size));
-		comparison = (*compar) (key, p, arg);
-
-		if (comparison < 0)
-			u = idx;
-		else if (comparison > 0)
-			l = idx + 1;
-		else
-			return (void *) p;
-	}
-
-	return NULL;
-}
-
 /*
  * build_attnums_array
  *		Transforms a bitmap into an array of AttrNumber values.
diff --git a/src/include/port.h b/src/include/port.h
index 227ef4b148..82f63de325 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -508,6 +508,11 @@ typedef int (*qsort_arg_comparator) (const void *a, const void *b, void *arg);
 extern void qsort_arg(void *base, size_t nel, size_t elsize,
 					  qsort_arg_comparator cmp, void *arg);
 
+extern void *bsearch_arg(const void *key, const void *base,
+						 size_t nmemb, size_t size,
+						 int (*compar) (const void *, const void *, void *),
+						 void *arg);
+
 /* port/chklocale.c */
 extern int	pg_get_encoding_from_locale(const char *ctype, bool write_message);
 
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index c849bd57c0..a0a3cf5b0f 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -85,11 +85,6 @@ extern int	multi_sort_compare_dims(int start, int end, const SortItem *a,
 extern int	compare_scalars_simple(const void *a, const void *b, void *arg);
 extern int	compare_datums_simple(Datum a, Datum b, SortSupport ssup);
 
-extern void *bsearch_arg(const void *key, const void *base,
-						 size_t nmemb, size_t size,
-						 int (*compar) (const void *, const void *, void *),
-						 void *arg);
-
 extern AttrNumber *build_attnums_array(Bitmapset *attrs, int *numattrs);
 
 extern SortItem *build_sorted_items(int numrows, int *nitems, HeapTuple *rows,
diff --git a/src/port/Makefile b/src/port/Makefile
index e41b005c4f..52dbf5783f 100644
--- a/src/port/Makefile
+++ b/src/port/Makefile
@@ -40,6 +40,7 @@ LIBS += $(PTHREAD_LIBS)
 OBJS = \
 	$(LIBOBJS) \
 	$(PG_CRC32C_OBJS) \
+	bsearch_arg.o \
 	chklocale.o \
 	erand48.o \
 	inet_net_ntop.o \
diff --git a/src/port/bsearch_arg.c b/src/port/bsearch_arg.c
new file mode 100644
index 0000000000..d24dc4b7c4
--- /dev/null
+++ b/src/port/bsearch_arg.c
@@ -0,0 +1,40 @@
+/*
+ *	bsearch_arg.c: bsearch variant with a user-supplied pointer
+ *
+ *	src/port/bsearch_arg.c
+ */
+
+
+#include "c.h"
+
+
+/* simple counterpart to qsort_arg */
+void *
+bsearch_arg(const void *key, const void *base, size_t nmemb, size_t size,
+			int (*compar) (const void *, const void *, void *),
+			void *arg)
+{
+	size_t		l,
+				u,
+				idx;
+	const void *p;
+	int			comparison;
+
+	l = 0;
+	u = nmemb;
+	while (l < u)
+	{
+		idx = (l + u) / 2;
+		p = (void *) (((const char *) base) + (idx * size));
+		comparison = (*compar) (key, p, arg);
+
+		if (comparison < 0)
+			u = idx;
+		else if (comparison > 0)
+			l = idx + 1;
+		else
+			return (void *) p;
+	}
+
+	return NULL;
+}
-- 
2.26.2

0002-Pass-all-scan-keys-to-BRIN-consistent-funct-20210308.patchtext/x-patch; charset=UTF-8; name=0002-Pass-all-scan-keys-to-BRIN-consistent-funct-20210308.patchDownload
From 9c5f976882745236875680e92ecc8d96dca80891 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 2/8] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c          | 158 +++++++++++++++++++-----
 src/backend/access/brin/brin_validate.c |   4 +-
 2 files changed, 126 insertions(+), 36 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 27ba596c6e..f9a0476024 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -390,6 +390,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey   **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -411,6 +414,66 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure. We allocate space for all attributes, so
+	 * that we don't have to bother determining which attributes are used.
+	 *
+	 * XXX The widest table can have ~1600 attributes, so this may allocate a
+	 * couple kilobytes of memory). We could invent a more compact approach
+	 * (with just space for used attributes) but that would make the matching
+	 * more complicated, so it may not be a win.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation used in the
+		 * index column (but only if the search is not IS NULL/ IS NOT NULL).
+		 * Otherwise we shouldn't be using this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno - 1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many scan keys
+			 * are there for this attribute, so we simply allocate the largest
+			 * number possible. This may waste a bit of memory, but we only
+			 * expect small number of scan keys in general, so this should be
+			 * negligible, and it's cheaper than having to repalloc
+			 * repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -471,7 +534,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int			attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -481,51 +544,78 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any scan keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of
+					 * multiple scan keys. We can determine that based on the
+					 * number of arguments - functions with extra parameter
+					 * (number of scan keys) do support this, otherwise we
+					 * have to simply pass the scan keys one by one, as
+					 * before.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same
+						 * for all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet
+						 * the criteria for a single one of them is enough to
+						 * discard the range as a whole, so break out of the
+						 * loop as soon as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+
+							/* mismatching key, no need to look further  */
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..11835d85cd 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-- 
2.26.2

0003-Process-all-scan-keys-in-existing-BRIN-opcl-20210308.patchtext/x-patch; charset=UTF-8; name=0003-Process-all-scan-keys-in-existing-BRIN-opcl-20210308.patchDownload
From 31116fc48cf8dc22dea06560319ba253a8e2ed21 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 5 Mar 2021 00:45:33 +0100
Subject: [PATCH 3/8] Process all scan keys in existing BRIN opclasses

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.
---
 src/backend/access/brin/brin_inclusion.c | 140 ++++++++++++++++-------
 src/backend/access/brin/brin_minmax.c    |  92 ++++++++++++---
 src/include/catalog/pg_proc.dat          |   4 +-
 3 files changed, 177 insertions(+), 59 deletions(-)

diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..a260074c91 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -251,6 +253,10 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 /*
  * BRIN inclusion consistent function
  *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches all those keys, we check the regular scan keys.
+ *
  * All of the strategies are optional.
  */
 Datum
@@ -258,24 +264,31 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/* Handle IS NULL/IS NOT NULL tests */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -284,7 +297,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -293,7 +311,14 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
@@ -301,10 +326,45 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all regular scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
+	/* This should be called only for regular keys, not for IS [NOT] NULL. */
+	Assert(!(key->sk_flags & SK_ISNULL));
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +384,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +444,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..e116084a02 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -140,29 +142,41 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's min/max
  * values.  Return true if so, false otherwise.
+ *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches all those keys, we check the regular scan keys.
  */
 Datum
 brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
 	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -171,7 +185,12 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -180,13 +199,52 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
+	}
+
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
+
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +287,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 506689d8ac..ad11a2b66f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8206,7 +8206,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8222,7 +8222,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0004-Move-IS-NOT-NULL-handling-from-BRIN-support-20210308.patchtext/x-patch; charset=UTF-8; name=0004-Move-IS-NOT-NULL-handling-from-BRIN-support-20210308.patchDownload
From 2a4f3633053020a312eb67f6ad1412d9548e01a4 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 2 Mar 2021 19:27:48 +0100
Subject: [PATCH 4/8] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c |  96 +-------
 src/backend/access/brin/brin_minmax.c    |  93 +------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 244 insertions(+), 241 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index f9a0476024..9f2656b8d9 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+								BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -179,7 +182,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -243,31 +245,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -390,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey   **keys;
-	int		   *nkeys;
+	ScanKey   **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -419,13 +399,18 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * consistent support procedure. We allocate space for all attributes, so
 	 * that we don't have to bother determining which attributes are used.
 	 *
+	 * We keep null and regular keys separate, so that we can pass just the
+	 * regular keys to the consistent function easily.
+	 *
 	 * XXX The widest table can have ~1600 attributes, so this may allocate a
 	 * couple kilobytes of memory). We could invent a more compact approach
 	 * (with just space for used attributes) but that would make the matching
 	 * more complicated, so it may not be a win.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -445,23 +430,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno - 1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan keys are
+		 * there for a given attribute, so we simply allocate the largest
+		 * number possible (as if all scan keys belonged to the same
+		 * attribute). This may waste a bit of memory, but we only expect
+		 * small number of scan keys in general, so this should be negligible,
+		 * and it's probably cheaper than having to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many scan keys
-			 * are there for this attribute, so we simply allocate the largest
-			 * number possible. This may waste a bit of memory, but we only
-			 * expect small number of scan keys in general, so this should be
-			 * negligible, and it's cheaper than having to repalloc
-			 * repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -469,9 +454,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -549,15 +548,58 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any scan keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular and
+					 * IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine
+					 * mismatch within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all
+					 * passed. If there are no regular scan keys, we're done -
+					 * the page range matches. If there are regular keys, but
+					 * the page range is marked as 'all nulls' it can't
+					 * possibly pass (we're assuming the operators are
+					 * strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -703,7 +745,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -732,25 +773,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1529,6 +1553,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1582,3 +1639,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values; our
+	 * deformed tuple will get updated if the new tuple doesn't fit the
+	 * original range (note this means we can't break out of the loop early).
+	 * Make a note of whether this happens, so that we know to insert the
+	 * modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's the
+			 * first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index a260074c91..b17077703c 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -268,52 +258,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* Handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
 
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
-
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -331,9 +278,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -574,37 +520,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index e116084a02..330bed0487 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -156,52 +146,9 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
 
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -215,9 +162,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -307,34 +253,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 78c89a6961..79440ebe7b 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0005-Optimize-allocations-in-bringetbitmap-20210308.patchtext/x-patch; charset=UTF-8; name=0005-Optimize-allocations-in-bringetbitmap-20210308.patchDownload
From 50351722b822f6485785f91c50627fdc28b50266 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 2 Mar 2021 19:57:27 +0100
Subject: [PATCH 5/8] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 60 ++++++++++++++++++++++++++--------
 1 file changed, 47 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 9f2656b8d9..1f82e965f9 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -402,15 +405,52 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * We keep null and regular keys separate, so that we can pass just the
 	 * regular keys to the consistent function easily.
 	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly the
+	 * same lifetime, so that's OK.
+	 *
 	 * XXX The widest table can have ~1600 attributes, so this may allocate a
 	 * couple kilobytes of memory). We could invent a more compact approach
 	 * (with just space for used attributes) but that would make the matching
 	 * more complicated, so it may not be a win.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* regular keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* NULL keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -444,9 +484,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -457,17 +497,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0006-BRIN-bloom-indexes-20210308.patchtext/x-patch; charset=UTF-8; name=0006-BRIN-bloom-indexes-20210308.patchDownload
From dd51e238f5e00da3bf245402c3c2167fc7ea80a4 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 6/8] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 230 ++++++-
 doc/src/sgml/ref/create_index.sgml        |   1 +
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 787 ++++++++++++++++++++++
 src/include/access/brin.h                 |   2 +
 src/include/access/brin_internal.h        |   4 +
 src/include/catalog/pg_amop.dat           | 116 ++++
 src/include/catalog/pg_amproc.dat         | 447 ++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 ++
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +-
 src/test/regress/expected/brin_bloom.out  | 428 ++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 376 +++++++++++
 19 files changed, 2555 insertions(+), 7 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 06880c0f7b..58126a5a3c 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -115,7 +115,8 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
   operator classes store the minimum and the maximum values appearing
   in the indexed column within the range.  The <firstterm>inclusion</firstterm>
   operator classes store a value which includes the values in the indexed
-  column within the range.
+  column within the range.  The <firstterm>bloom</firstterm> operator
+  classes build a Bloom filter for all values in the range.
  </para>
 
  <table id="brin-builtin-opclasses-table">
@@ -128,6 +129,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +159,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +173,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +187,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +201,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +215,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +229,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +253,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +267,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +281,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +295,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +309,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +323,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +337,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +351,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +365,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +379,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +393,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +425,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +439,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +453,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +467,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +481,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +495,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +509,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -409,6 +534,55 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    </tbody>
   </tgroup>
  </table>
+
+  <sect2 id="brin-builtin-opclasses--parameters">
+   <title>Operator Class Parameters</title>
+
+   <para>
+    Some of the built-in operator classes allow specifying parameters affecting
+    behavior of the operator class.  Each operator class has its own set of
+    allowed parameters.  Only the <literal>bloom</literal> operator class
+    allows specifying parameters:
+   </para>
+
+   <para>
+    <acronym>bloom</acronym> operator classes accept these parameters:
+   </para>
+
+   <variablelist>
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default value is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be
+     between 0.0001 and 0.25. The default value is 0.01, which is 1% false
+     positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   </variablelist>
+  </sect2>
+
 </sect1>
 
 <sect1 id="brin-extensibility">
@@ -779,6 +953,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 51b4d57939..4a29935785 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -581,6 +581,7 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..4494484b3b
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,787 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test whether it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_page.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001	/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25	/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+
+#define BloomMaxFilterSize \
+	MAXALIGN_DOWN(BLCKSZ - \
+				  (MAXALIGN(SizeOfPageHeaderData + \
+							sizeof(ItemIdData)) + \
+				   MAXALIGN(sizeof(BrinSpecialSpace)) + \
+				   SizeOfBrinTuple))
+
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * To calculate the additional hashes (treating the uint32 hash as an input
+ * value), we use the approach with two hash functions from this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions are calculated using hard-coded seeds.
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes
+ * that are not entirely 0. But while indexes don't support TOAST, the
+ * varlena can still be compressed. So this seems unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high. But even if the false positive rate
+ * exceeds the desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16		flags;
+
+	/* fields for the HASHED phase */
+	uint8		nhashes;		/* number of hash functions */
+	uint32		nbits;			/* number of bits in the bitmap (size) */
+	uint32		nbits_set;		/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+
+}			BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size		len;
+	BloomFilter *filter;
+
+	int			nbits;			/* size of filter / number of bits */
+	int			nbytes;			/* size of filter / number of bytes */
+
+	double		k;				/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	nbits = ceil(-(ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	nbytes = ((nbits + 7) / 8);
+	nbits = nbytes * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * Initially the bloom filter is just zeroes and so very compressible, but
+	 * as we add values it gets more and more random, and so less and less
+	 * compressible. So initially everything fits on the page, but we might
+	 * get surprising failures later - we want to prevent that, so we reject
+	 * bloom filter that are obviously too large.
+	 *
+	 * XXX It's not uncommon to oversize the bloom filter a bit, to defend
+	 * against unexpected data anomalies (parts of table with more distinct
+	 * values per range etc.). But we still need to make sure even the
+	 * oversized filter fits on page, if such need arises.
+	 *
+	 * XXX This check is not perfect, because the index may have multiple
+	 * filters that are small individually, but too large when combined.
+	 */
+	if (nbytes > BloomMaxFilterSize)
+		elog(ERROR, "the bloom filter is too large (%d > %zu)", nbytes,
+			 BloomMaxFilterSize);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * nbits / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so the
+	 * varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + nbytes;
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = nbits;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter * filter, uint32 value, bool *updated)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (!(filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter * filter, uint32 value)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* calculate the two hashes */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (!(filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash), but
+	 * let's keep the array just like inclusion and minman opclasses, for
+	 * consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+}			BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+									uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double		ndistinct;
+	double		maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this case to
+	 * maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a couple of
+	 * safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples, in the
+	 * range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from
+	 * BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to
+				 * return the current page range if the minimum value in the
+				 * range <= scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters for now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing text
+	 * input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 79440ebe7b..8cc4e532e6 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..04d678f96a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,20 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2040,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2062,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2084,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2155,14 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2180,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2202,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2224,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2266,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2288,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2439,20 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2470,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2492,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2548,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2570,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2636,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..6709c8dfea 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..dea9adaf98 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9901',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '9902',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '9903',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9904',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9905',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '9906',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '9907',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '9908',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9909',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9910',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9911',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9912',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9913',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '9914',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '9915',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9916',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9917',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9918',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9919',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ad11a2b66f..36c1433379 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8229,6 +8229,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '9920', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '9921', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '9922', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '9923', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '9924', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11402,4 +11422,18 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8959c2f53b..74e279cbf9 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,5 +679,10 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-
+{ oid => '9925',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..32c56a996a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,428 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..e568b9fea2 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9925 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index c77b0d7342..ecd0806718 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 0264a97324..c0d7fa76f1 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..5d499208e3
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,376 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0007-BRIN-minmax-multi-indexes-20210308.patchtext/x-patch; charset=UTF-8; name=0007-BRIN-minmax-multi-indexes-20210308.patchDownload
From 74ec8c94e5e6af51bc226987029ec424df87de73 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Wed, 3 Feb 2021 19:00:00 +0100
Subject: [PATCH 7/8] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  276 +-
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_bloom.c        |    4 +-
 src/backend/access/brin/brin_minmax_multi.c | 2982 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   33 +-
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    8 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 ++++
 src/include/catalog/pg_amproc.dat           |  600 +++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   85 +
 src/include/catalog/pg_type.dat             |    6 +
 src/test/regress/expected/brin_multi.out    |  445 +++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  397 +++
 21 files changed, 5465 insertions(+), 30 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 58126a5a3c..1fbdcbeee4 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -116,7 +116,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
   in the indexed column within the range.  The <firstterm>inclusion</firstterm>
   operator classes store a value which includes the values in the indexed
   column within the range.  The <firstterm>bloom</firstterm> operator
-  classes build a Bloom filter for all values in the range.
+  classes build a Bloom filter for all values in the range.  The
+  <firstterm>minmax-multi</firstterm> operator classes store multiple
+  minimum and maximum values, representing values appearing in the indexed
+  column within the range.
  </para>
 
  <table id="brin-builtin-opclasses-table">
@@ -215,6 +218,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -229,6 +241,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -243,6 +264,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -267,6 +297,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -281,6 +320,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -295,6 +343,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -309,6 +366,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -323,6 +389,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -337,6 +412,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -351,6 +435,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -379,6 +472,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -393,6 +495,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -407,6 +518,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -453,6 +573,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -467,6 +596,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -481,6 +619,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -495,6 +642,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -509,6 +665,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -523,6 +688,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -581,6 +755,25 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    </varlistentry>
 
    </variablelist>
+
+   <para>
+    <acronym>minmax-multi</acronym> operator classes accept these parameters:
+   </para>
+
+   <variablelist>
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     either a point, or a boundary of an interval. Values must be between
+     8 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   </variablelist>
   </sect2>
 
 </sect1>
@@ -704,13 +897,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, minmax-multi, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -1007,6 +1201,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The minmax-multi operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, minmax-multi allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the minmax-multi support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-minmax-multi-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-minmax-multi-table">
+  <title>Procedure and Support Numbers for minmax-multi Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 4494484b3b..c086e83236 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -83,7 +83,7 @@
  * the whole index tuple that has to fit into a page. And for multi-column
  * indexes that may include pieces we have no control over (not necessarily
  * bloom filters, the other columns may use other BRIN opclasses). So it's
- * not entirely clear how to distrubute the space between those columns.
+ * not entirely clear how to distribute the space between those columns.
  *
  * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
  * make some basic sizing decisions, based on the size of BRIN ranges, and
@@ -468,7 +468,7 @@ brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
 
 	/*
 	 * Positive values are to be used directly, but we still apply a couple of
-	 * safeties no to use unreasonably small bloom filters.
+	 * safeties to avoid using unreasonably small bloom filters.
 	 */
 	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
 
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..d2dc38adc8
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2982 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With minmax-multi opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 50% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values */
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Sizing the insert buffer - we use 10x the number of values specified
+ * in the reloption, but we cap it to 8192 not to get too large. When
+ * the buffer gets full, we reduce the number of values by half.
+ */
+#define		MINMAX_BUFFER_FACTOR			10
+#define		MINMAX_BUFFER_MIN				256
+#define		MINMAX_BUFFER_MAX				8192
+#define		MINMAX_BUFFER_LOAD_FACTOR		0.5
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+}			MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange; /* number of values per range */
+}			MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0))
+
+/*
+ * The summary of minmax-multi indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* Cache information that we need quite often. */
+	Oid			typid;
+	Oid			colloid;
+	AttrNumber	attno;
+	FmgrInfo   *cmp;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nsorted;		/* number of sorted values (ranges + points) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/*
+	 * We simply add the values into a large buffer, without any expensive
+	 * steps (sorting, deduplication, ...). The buffer is a multiple of the
+	 * target number of values, so the compaction happen less often,
+	 * amortizing the costs. We keep the actual target and compact to the
+	 * requested number of values at the very end, before serializing to
+	 * on-disk representation.
+	 */
+	/* requested number of values */
+	int			target_maxvalues;
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum		values[FLEXIBLE_ARRAY_MEMBER];
+}			Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, with a simple header
+ * with basic metadata, followed by the boundary values. It has a varlena
+ * header, so can be treated as varlena directly.
+ *
+ * See range_serialize/range_deserialize for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* type of values stored in the data array */
+	Oid			typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+}			SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(int maxvalues, SerializedRanges *range);
+
+
+/*
+ * Used to represent ranges expanded to make merging and combining easier.
+ *
+ * Each expanded range is essentially an interval, represented by min/max
+ * values, along with a flag whether it's a collapsed range (in which case
+ * the min and max values are equal). We have the flag to handle by-ref
+ * data types - we can't simply compare the datums, and this saves some
+ * calls to the type-specific comparator function.
+ */
+typedef struct ExpandedRange
+{
+	Datum		minval;			/* lower boundary */
+	Datum		maxval;			/* upper boundary */
+	bool		collapsed;		/* true if minval==maxval */
+}			ExpandedRange;
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of extended ranges).
+ */
+typedef struct DistanceValue
+{
+	int			index;
+	double		value;
+}			DistanceValue;
+
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+										   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+													uint16 attno, Oid subtype,
+													uint16 strategynum);
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+}			compare_context;
+
+static int	compare_values(const void *a, const void *b, void *arg);
+
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+	int			i;
+	Datum		lt;
+
+	for (i = 0; i < (nvalues - 1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i + 1]);
+		Assert(DatumGetBool(lt));
+	}
+}
+#endif
+
+/*
+ * Comprehensive check of the Ranges structure.
+ */
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+
+	/* some basic sanity checks */
+	Assert(ranges->nranges >= 0);
+	Assert(ranges->nsorted >= 0);
+	Assert(ranges->nvalues >= ranges->nsorted);
+	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
+	Assert(ranges->typid != InvalidOid);
+
+	/*
+	 * First the ranges - there are 2*nranges boundary values, and the values
+	 * have to be strictly ordered (equal values would mean the range is
+	 * collapsed, and should be stored as a point). This also guarantees that
+	 * the ranges do not overlap.
+	 */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2 * ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2 * ranges->nranges],
+					 ranges->nsorted);
+
+	/*
+	 * Check that none of the values are not covered by ranges (both sorted
+	 * and unsorted)
+	 */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		compar;
+		int			start,
+					end;
+		Datum		minvalue,
+					maxvalue;
+
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nranges == 0)
+			break;
+
+		minvalue = ranges->values[0];
+		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+		/* smaller than the smallest value in the first range */
+		if (DatumGetBool(compar))
+			continue;
+
+		/*
+		 * Is the value greater than the maxval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+		/* larger than the largest value in the last range */
+		if (DatumGetBool(compar))
+			continue;
+
+		start = 0;				/* first range */
+		end = ranges->nranges - 1;	/* last range */
+		while (true)
+		{
+			int			midpoint = (start + end) / 2;
+
+			/* this means we ran out of ranges in the last step */
+			if (start > end)
+				break;
+
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2 * midpoint];
+			maxvalue = ranges->values[2 * midpoint + 1];
+
+			/*
+			 * Is the value smaller than the minval? If yes, we'll recurse to
+			 * the left side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+			{
+				end = (midpoint - 1);
+				continue;
+			}
+
+			/*
+			 * Is the value greater than the minval? If yes, we'll recurse to
+			 * the right side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+			{
+				start = (midpoint + 1);
+				continue;
+			}
+
+			/* hey, we found a matching range */
+			Assert(false);
+		}
+	}
+
+	/* and values in the unsorted part must not be in sorted part */
+	for (i = ranges->nsorted; i < ranges->nvalues; i++)
+	{
+		compare_context cxt;
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nsorted == 0)
+			break;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		Assert(bsearch_arg(&value, &ranges->values[2 * ranges->nranges],
+						   ranges->nsorted, sizeof(Datum),
+						   compare_values, (void *) &cxt) == NULL);
+	}
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidExpandedRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						  Form_pg_attribute attr, ExpandedRange *ranges,
+						  int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+	FmgrInfo   *eq;
+	FmgrInfo   *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum		r;
+		Datum		minval = ranges[i].minval;
+		Datum		maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else					/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e. upper <
+	 * lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges - 1; i++)
+	{
+		Datum		r;
+		Datum		maxval = ranges[i].maxval;
+		Datum		minval = ranges[i + 1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size		len;
+	Ranges	   *ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values); /* fixed header */
+	len += maxvalues * sizeof(Datum);	/* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+
+/*
+ * range_deduplicate_values
+ *		Deduplicate the part with values in the simple points.
+ *
+ * This is meant to be a cheaper way of reducing the size of the ranges. It
+ * does not touch the ranges, and only sorts the other values - it does not
+ * call the distance functions, which may be quite expensive, etc.
+ */
+static void
+range_deduplicate_values(Ranges *range)
+{
+	int			i,
+				n;
+	int			start;
+	compare_context cxt;
+
+	/*
+	 * If there are no unsorted values, we're done (this probably can't
+	 * happen, as we're adding values to unsorted part).
+	 */
+	if (range->nsorted == range->nvalues)
+		return;
+
+	/* sort the values */
+	cxt.colloid = range->colloid;
+	cxt.cmpFn = range->cmp;
+
+	/* how many values to sort? */
+	start = 2 * range->nranges;
+
+	qsort_arg(&range->values[start],
+			  range->nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	n = 1;
+	for (i = 1; i < range->nvalues; i++)
+	{
+		/* same as preceding value, so store it */
+		if (compare_values(&range->values[start + i - 1],
+						   &range->values[start + i],
+						   (void *) &cxt) == 0)
+			continue;
+
+		range->values[start + n] = range->values[start + i];
+
+		n++;
+	}
+
+	/* now all the values are sorted */
+	range->nvalues = n;
+	range->nsorted = n;
+
+	AssertCheckRanges(range, range->cmp, range->colloid);
+}
+
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size		len;
+	int			nvalues;
+	SerializedRanges *serialized;
+	Oid			typid;
+	int			typlen;
+	bool		typbyval;
+
+	int			i;
+	char	   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nsorted >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+	Assert(range->target_maxvalues > 0);
+
+	/* at this point the range should be compacted to the target size */
+	Assert(2 * range->nranges + range->nvalues <= range->target_maxvalues);
+
+	Assert(range->target_maxvalues <= range->maxvalues);
+
+	/* range boundaries are always sorted */
+	Assert(range->nvalues >= range->nsorted);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2 * range->nranges + range->nvalues;
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges, data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)			/* varlena */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)		/* cstring */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else						/* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->target_maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation this
+	 * depends on the particular data type).
+	 */
+	ptr = serialized->data;		/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int			tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int			tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + len));
+	}
+
+	/* exact size */
+	Assert(ptr == ((char *) serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(int maxvalues, SerializedRanges *serialized)
+{
+	int			i,
+				nvalues;
+	char	   *ptr;
+	bool		typbyval;
+	int			typlen;
+
+	Ranges	   *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2 * serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+	Assert(serialized->maxvalues <= maxvalues);
+
+	range = minmax_multi_init(maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->nsorted = serialized->nvalues;
+	range->maxvalues = maxvalues;
+	range->target_maxvalues = serialized->maxvalues;
+
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need to copy
+	 * the values and will instead just point the values to the serialized
+	 * varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *) serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+/*
+ * compare_expanded_ranges
+ *	  Compare the expanded ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_expanded_ranges(const void *a, const void *b, void *arg)
+{
+	ExpandedRange *ra = (ExpandedRange *) a;
+	ExpandedRange *rb = (ExpandedRange *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * compare_values
+ *	  Compare the values.
+ */
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum	   *da = (Datum *) a;
+	Datum	   *db = (Datum *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *da, *db);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *db, *da);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Check if the new value matches one of the existing ranges.
+ */
+static bool
+has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges,
+				   Datum newval, AttrNumber attno, Oid typid)
+{
+	Datum		compar;
+
+	Datum		minvalue = ranges->values[0];
+	Datum		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+
+	/* binary search on ranges */
+	int			start,
+				end;
+
+	if (ranges->nranges == 0)
+		return false;
+
+	/*
+	 * Otherwise, need to compare the new value with boundaries of all the
+	 * ranges. First check if it's less than the absolute minimum, which is
+	 * the first value in the array.
+	 */
+	cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												   BTLessStrategyNumber);
+	compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+	/* smaller than the smallest value in the range list */
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * And now compare it to the existing maximum (last value in the data
+	 * array). But only if we haven't already ruled out a possible match in
+	 * the minvalue check.
+	 */
+	cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													  BTGreaterStrategyNumber);
+	compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * So we know it's in the general min/max, the question is whether it
+	 * falls in one of the ranges or gaps. We'll do a binary search on
+	 * individual ranges - for each range we check equality (value falls
+	 * into the range), and then check ranges either above or below the
+	 * current range.
+	 */
+	start = 0;					/* first range */
+	end = (ranges->nranges - 1);	/* last range */
+	while (true)
+	{
+		int			midpoint = (start + end) / 2;
+
+		/* this means we ran out of ranges in the last step */
+		if (start > end)
+			return false;
+
+		/* copy the min/max values from the ranges */
+		minvalue = ranges->values[2 * midpoint];
+		maxvalue = ranges->values[2 * midpoint + 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in this range */
+		if (DatumGetBool(compar))
+		{
+			end = (midpoint - 1);
+			continue;
+		}
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+		/* larger than the largest value in this range */
+		if (DatumGetBool(compar))
+		{
+			start = (midpoint + 1);
+			continue;
+		}
+
+		/* hey, we found a matching range */
+		return true;
+	}
+
+	return false;
+}
+
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX We only inspect the sorted parts, which means we may add duplicate
+ * values. It may produce some false negatives when adding the values, but
+ * only after we already added some values (otherwise there is no unsorted
+ * part). And when querying the index, there should be no unsorted values,
+ * because the values are sorted and deduplicated during serialization.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+					 AttrNumber attno, Form_pg_attribute attr,
+					 Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid))
+		return true;
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													BTEqualStrategyNumber);
+
+	/*
+	 * There is no matching range, so let's inspect the sorted values.
+	 *
+	 * XXX We do a sequential search for small number of values, and binary
+	 * search once we have more than 16 values. This threshold is somewhat
+	 * arbitrary, as it depends on how expensive the comparison function is.
+	 * So maybe we should just do the binary search all the time.
+	 *
+	 * XXX If we use the threshold here, maybe we should do the same thing in
+	 * has_matching_range?
+	 */
+	if (ranges->nsorted >= 16)
+	{
+		compare_context cxt;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		if (bsearch_arg(&newval, &ranges->values[2 * ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) != NULL)
+			return true;
+	}
+	else
+	{
+		for (i = 2 * ranges->nranges; i < 2 * ranges->nranges + ranges->nsorted; i++)
+		{
+			Datum		compar;
+
+			compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+			/* found an exact match */
+			if (DatumGetBool(compar))
+				return true;
+		}
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * Expand ranges from Ranges into ExpandedRange array. This expects the
+ * eranges to be pre-allocated and with the correct size - there needs to be
+ * (nranges + nvalues) elements.
+ *
+ * The order of expanded ranges is arbitrary. We do expand the ranges first,
+ * and this part is sorted. But then we expand the values, and this part may
+ * be unsorted.
+ */
+static void
+fill_expanded_ranges(ExpandedRange *eranges, int neranges, Ranges *ranges)
+{
+	int			idx;
+	int			i;
+
+	/* Check that the output array has the right size. */
+	Assert(neranges == (2 * ranges->nranges + ranges->nvalues));
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * i];
+		eranges[idx].maxval = ranges->values[2 * i + 1];
+		eranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].maxval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	/* Did we produce the expected number of elements? */
+	Assert(idx == neranges);
+
+	return;
+}
+
+/*
+ * Sort and deduplicate expanded ranges.
+ *
+ * The ranges may be deduplicated - we're simply appending values, without
+ * checking for duplicates etc. So maybe the deduplication will reduce the
+ * number of ranges enough, and we won't have to compute the distances etc.
+ *
+ * Returns the number of expanded ranges.
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that some of the input data is already sorted (all the ranges
+ * and possibly some of the points) and do merge sort.
+ */
+static int
+sort_expanded_ranges(FmgrInfo *cmp, Oid colloid,
+					 ExpandedRange *eranges, int neranges)
+{
+	int			n;
+	int			i;
+	compare_context cxt;
+
+	Assert(neranges > 0);
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(eranges, neranges, sizeof(ExpandedRange),
+			  compare_expanded_ranges, (void *) &cxt);
+
+	/*
+	 * Deduplicate the ranges - simply compare each range to the preceding
+	 * one, and skip the duplicate ones.
+	 */
+	n = 1;
+	for (i = 1; i < neranges; i++)
+	{
+		/* if the current range is equal to the preceding one, do nothing */
+		if (!compare_expanded_ranges(&eranges[i - 1], &eranges[i], (void *) &cxt))
+			continue;
+
+		/* otherwise copy it to n-th place (if not already there) */
+		if (i != n)
+			memcpy(&eranges[n], &eranges[i], sizeof(ExpandedRange));
+
+		n++;
+	}
+
+	Assert((n > 0) && (n <= neranges));
+
+	return n;
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the expanded ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_overlapping_ranges(FmgrInfo *cmp, Oid colloid,
+						 ExpandedRange *eranges, int neranges)
+{
+	int			idx;
+
+	/* Merge ranges (idx) and (idx+1) if they overlap. */
+	idx = 0;
+	while (idx < (neranges - 1))
+	{
+		Datum		r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap if (minval
+		 * < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges are
+		 * ordered, so there are no more overlaps, because all the remaining
+		 * ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they overlap only partially.
+		 * So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].maxval);
+
+		if (DatumGetBool(r))
+			eranges[idx].maxval = eranges[idx + 1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectively of the
+		 * previous state).
+		 */
+		eranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the remaining
+		 * ranges by 1. There are ncranges elements, and we need to move
+		 * elements from (idx+2). That means the number of elements to move is
+		 * [ncranges - (idx+2)].
+		 */
+		memmove(&eranges[idx + 1], &eranges[idx + 2],
+				(neranges - (idx + 2)) * sizeof(ExpandedRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range, as
+		 * it might overlap with additional ranges thanks to the merge).
+		 */
+		neranges--;
+	}
+
+	return neranges;
+}
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ * This is intentionally sorting the distances in descending order, i.e.
+ * the longer gaps will be at the front.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *) a;
+	DistanceValue *db = (DistanceValue *) b;
+
+	if (da->value < db->value)
+		return 1;
+	else if (da->value > db->value)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Given an array of expanded ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (max-min) for pairs
+ * of consecutive ranges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_expanded_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			ndistances;
+	DistanceValue *distances;
+
+	Assert(neranges >= 2);
+
+	ndistances = (neranges - 1);
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * ndistances);
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges so
+	 * that we can sort them once.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		Datum		a1,
+					a2,
+					r;
+
+		a1 = eranges[i].maxval;
+		a2 = eranges[i + 1].minval;
+
+		/* compute length of the gap (between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index of the gap the distance is for */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/*
+	 * Sort the distances in descending order, so that the longest gaps are at
+	 * the front.
+	 */
+	pg_qsort(distances, ndistances, sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds expanded ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).  This expanded
+ * representation makes the processing a bit easier, as it allows handling
+ * ranges and points the same way.
+ *
+ * We sort and deduplicate the expanded ranges - this is necessary, because
+ * the points may be unsorted. And moreover the two parts (ranges and
+ * points) are sorted on their own.
+ */
+static ExpandedRange *
+build_expanded_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					  int *nranges)
+{
+	int			neranges;
+	ExpandedRange *eranges;
+
+	/* both ranges and points are expanded into a separate element */
+	neranges = ranges->nranges + ranges->nvalues;
+
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges */
+	fill_expanded_ranges(eranges, neranges, ranges);
+
+	/* sort and deduplicate the expanded ranges */
+	neranges = sort_expanded_ranges(cmp, colloid, eranges, neranges);
+
+	/* remember how many cranges we built */
+	*nranges = neranges;
+
+	return eranges;
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Counts boundary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(ExpandedRange *cranges, int ncranges)
+{
+	int			i;
+	int			count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+#endif
+
+/*
+ * reduce_expanded_ranges
+ *		reduce the ranges until the number of values is low enough
+ *
+ * Combines ranges until the number of boundary values drops below the
+ * threshold specified by max_values. This happens by merging enough
+ * ranges by distance between them.
+ *
+ * Returns the number of result ranges.
+ *
+ * We simply use the global min/max and then add boundaries for enough
+ * largest gaps. Each gap adds 2 values, so we simply use (target/2-1)
+ * distances. Then we simply sort all the values - each two values are
+ * a boundary of a range (possibly collapsed).
+ *
+ * XXX Some of the ranges may be collapsed (i.e. the min/max values are
+ * equal), but we ignore that for now. We could repeat the process,
+ * adding a couple more gaps recursively.
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ *
+ * XXX Or maybe we could just handle this by using random value as a
+ * tie-break, or by adding random noise to the actual distance.
+ */
+static int
+reduce_expanded_ranges(ExpandedRange *eranges, int neranges,
+					   DistanceValue *distances, int max_values,
+					   FmgrInfo *cmp, Oid colloid)
+{
+	int			i;
+	int			nvalues;
+	Datum	   *values;
+
+	compare_context cxt;
+
+	/* total number of gaps between ranges */
+	int			ndistances = (neranges - 1);
+
+	/* number of gaps to keep */
+	int			keep = (max_values / 2 - 1);
+
+	/*
+	 * Maybe we have sufficiently low number of ranges already?
+	 *
+	 * XXX This should happen before we actually do the expensive stuff like
+	 * sorting, so maybe this should be just an assert.
+	 */
+	if (keep >= ndistances)
+		return neranges;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	/* add the global min/max values, from the first/last range */
+	values[nvalues++] = eranges[0].minval;
+	values[nvalues++] = eranges[neranges - 1].maxval;
+
+	/* add boundary values for enough gaps */
+	for (i = 0; i < keep; i++)
+	{
+		/* index of the gap between (index) and (index+1) ranges */
+		int			index = distances[i].index;
+
+		Assert((index >= 0) && ((index + 1) < neranges));
+
+		/* add max from the preceding range, minval from the next one */
+		values[nvalues++] = eranges[index].maxval;
+		values[nvalues++] = eranges[index + 1].minval;
+
+		Assert(nvalues <= max_values);
+	}
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/*
+	 * Sort the values using the comparator function, and form ranges from the
+	 * sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		eranges[i].minval = values[2 * i];
+		eranges[i].maxval = values[2 * i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		eranges[i].collapsed = (compare_values(&values[2 * i],
+											   &values[2 * i + 1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+/*
+ * Store the boundary values from ExpandedRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_expanded_ranges(Ranges *ranges, ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (!eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->values[idx++] = eranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+
+	/* all the values are sorted */
+	ranges->nsorted = ranges->nvalues;
+
+	Assert(count_values(eranges, neranges) == 2 * ranges->nranges + ranges->nvalues);
+	Assert(2 * ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+}
+
+
+/*
+ * Consider freeing space in the ranges.
+ *
+ * Returns true if the value was actually modified.
+ */
+static bool
+ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *range)
+{
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	/*
+	 * If there is free space in the buffer, we're done without having to
+	 * modify anything.
+	 */
+	if (2 * range->nranges + range->nvalues < range->maxvalues)
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* did we reduce enough free space by just the deduplication? */
+	if (2 * range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR)
+		return true;
+
+	/*
+	 * We need to combine some of the existing ranges, to reduce the number of
+	 * values we have to store.
+	 *
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it (we might have
+	 * to do this repeatedly, even for a single BRIN page range). Otherwise
+	 * we'd have problems e.g. when building new indexes. So we use a memory
+	 * context and make sure we free the memory at the end (so if we call the
+	 * distance function many times, it might be an issue, but meh).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, colloid, range, &neranges);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * Combine ranges until we release at least 50% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR,
+									  cmpFn, colloid);
+
+	/* Make sure we've sufficiently reduced the number of ranges. */
+	Assert(count_values(eranges, neranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR);
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(range, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Did we break the ranges somehow? */
+	AssertCheckRanges(range, cmpFn, colloid);
+
+	return true;
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the minmax-multi range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn;
+	bool		modified = false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Make sure there's enough free space in the buffer. We only trigger this
+	 * when the buffer is full, which means it had to be modified as we size
+	 * it to be larger than what is stored on disk.
+	 *
+	 * XXX This needs to happen before we check if the value is contained in
+	 * the range, because the value might be in the unsorted part, and we
+	 * don't check that in range_contains_value. The deduplication would then
+	 * move it to the sorted part, and we'd add the value too, which violates
+	 * the rule that we never have duplicates with the ranges or sorted
+	 * values.
+	 *
+	 * XXX At the moment this only does the deduplication.
+	 *
+	 * XXX We might also deduplicate and recheck if the value is contained,
+	 * but that seems like an overkill. We'd need to deduplicate anyway, so
+	 * why not do it now.
+	 */
+	modified = ensure_free_space_in_buffer(bdesc, colloid,
+										   attno, attr, ranges);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then do the
+	 * deduplication in a batch, hoping for better efficiency. But that would
+	 * mean we actually modify the range every time, which means having to
+	 * serialize the value, which does palloc, walks the values, copies them,
+	 * etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming the
+	 * comparator function is not very expensive.
+	 *
+	 * This also implies the values array can't contain duplicate values.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return modified;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we do a
+	 * simple insertion sort. We could do something more elaborate, e.g. by
+	 * sorting the values only now and then, but for small counts (e.g. when
+	 * maxvalues is 64) this should be fine.
+	 */
+	ranges->values[2 * ranges->nranges + ranges->nvalues] = newval;
+	ranges->nvalues++;
+
+	/*
+	 * Check we haven't broken the ordering of boundary values (checks both
+	 * parts, but that doesn't hurt).
+	 */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/* Also check the range contains the value we just added. */
+	/* FIXME Assert(ranges, cmpFn, colloid); */
+
+	/* yep, we've modified the range */
+	return true;
+}
+
+/*
+ * Generate range representation of data collected during "batch mode".
+ * This is similar to reduce_expanded_ranges, except that we can't assume
+ * the values are sorted and there may be duplicate values.
+ */
+static void
+compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
+											   BTLessStrategyNumber);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, ranges->attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, ranges->colloid, ranges, &neranges);
+
+	/* FIXME this should do neranges >= max_values */
+	if (neranges > 1)
+	{
+		/* build array of gap distances and sort them in ascending order */
+		distances = build_distances(distanceFn, ranges->colloid,
+									eranges, neranges);
+
+		/*
+		 * Combine ranges until we get below max_values. We don't use any
+		 * scale factor, because this is used during serialization, and we
+		 * don't expect more tuples to be inserted anytime soon.
+		 */
+		neranges = reduce_expanded_ranges(eranges, neranges, distances,
+										  max_values, cmpFn, ranges->colloid);
+
+		Assert(count_values(eranges, neranges) <= max_values);
+	}
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(ranges, eranges, neranges);
+
+	/* check all the range invariants */
+	AssertCheckRanges(ranges, cmpFn, ranges->colloid);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float		a1 = PG_GETARG_FLOAT4(0);
+	float		a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double		a1 = PG_GETARG_FLOAT8(0);
+	double		a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16		a1 = PG_GETARG_INT16(0);
+	int16		a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32		a1 = PG_GETARG_INT32(0);
+	int32		a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64		a1 = PG_GETARG_INT64(0);
+	int64		a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double		da1,
+				da2;
+
+	ItemPointer pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	/*
+	 * We use the no-check variants here, because user-supplied values may
+	 * have (ip_posid == 0). See ItemPointerCompare.
+	 */
+	da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa1);
+
+	da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Computes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum		d;
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int			i;
+	float8		delta = 0;
+
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN - 1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute approximate distance between two dates.
+ */
+Datum
+brin_minmax_multi_distance_date(PG_FUNCTION_ARGS)
+{
+	DateADT		dateVal1 = PG_GETARG_DATEADT(0);
+	DateADT		dateVal2 = PG_GETARG_DATEADT(1);
+
+	if (DATE_NOT_FINITE(dateVal1) || DATE_NOT_FINITE(dateVal2))
+		PG_RETURN_FLOAT8(0);
+
+	PG_RETURN_FLOAT8(dateVal1 - dateVal2);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeADT		ta = PG_GETARG_TIMEADT(0);
+	TimeADT		tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+Datum
+brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Timestamp	dt1 = PG_GETARG_TIMESTAMP(0);
+	Timestamp	dt2 = PG_GETARG_TIMESTAMP(1);
+
+	if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
+		PG_RETURN_FLOAT8(0);
+
+	delta = dt2 - dt1;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Interval   *ia = PG_GETARG_INTERVAL_P(0);
+	Interval   *ib = PG_GETARG_INTERVAL_P(1);
+	Interval   *result;
+
+	result = (Interval *) palloc(sizeof(Interval));
+
+	result->month = ib->month - ia->month;
+	/* overflow check copied from int4mi */
+	if (!SAMESIGN(ib->month, ia->month) &&
+		!SAMESIGN(result->month, ib->month))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->day = ib->day - ia->day;
+	if (!SAMESIGN(ib->day, ia->day) &&
+		!SAMESIGN(result->day, ib->day))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->time = ib->time - ia->time;
+	if (!SAMESIGN(ib->time, ia->time) &&
+		!SAMESIGN(result->time, ib->time))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	/*
+	 * We assume months have 31 days - we don't need to be precise, in the
+	 * worst case we'll build somewhat less efficient ranges.
+	 */
+	delta = (float8) (result->month * 31 + result->day);
+
+	/* convert to microseconds (just like the time part) */
+	delta = 24L * 3600L * delta;
+
+	/* and add the time part */
+	delta += result->time / (float8) 1000000.0;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((float8) b->h - (float8) a->h);
+	delta /= 256;
+
+	delta += ((float8) b->g - (float8) a->g);
+	delta /= 256;
+
+	delta += ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from different families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+	int			i;
+	int			len;
+	unsigned char *addra,
+			   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be in
+	 * maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		PG_RETURN_FLOAT8(1.0);
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16;				/* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len - 1; i >= 0; i--)
+	{
+		delta += (float8) addrb[i] - (float8) addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
+{
+	Ranges	   *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s;
+
+	/*
+	 * In batch mode, we need to compress the accumulated values to the
+	 * actually requested number of values/ranges.
+	 */
+	compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+
+	s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges	   *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, if possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 *
+	 * When starting with an empty range, we assume this is a batch mode, i.e.
+	 * we size the buffer for the maximum possible number of items in the
+	 * range (based on range size and max number of items on a page).
+	 *
+	 * XXX This may require quite a bit of memory, so maybe we should use some
+	 * value in between. OTOH most tables will have much wider rows, so the
+	 * number of rows per page is much lower.
+	 *
+	 * XXX Maybe we should do this (using larger buffer) even when there
+	 * already is a summary?
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		int			target_maxvalues;
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		/* what was specified as a reloption? */
+		target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, target_maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+		ranges = minmax_multi_init(maxvalues);
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+		ranges->target_maxvalues = target_maxvalues;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, serialized->maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		ranges = range_deserialize(maxvalues, serialized);
+
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+
+	/* MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS(); */
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges	   *ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized->maxvalues, serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum		minval = ranges->values[2 * rangeno];
+		Datum		maxval = ranges->values[2 * rangeno + 1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+					{
+						Datum		compar;
+						FmgrInfo   *cmpFn;
+
+						/* by default this range does not match */
+						matches = false;
+
+						/*
+						 * Otherwise, need to compare the new value with
+						 * boundaries of all the ranges. First check if it's
+						 * less than the absolute minimum, which is the first
+						 * value in the array.
+						 */
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTLessStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+						/* smaller than the smallest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTGreaterStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+						/* larger than the largest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						/*
+						 * haven't managed to eliminate this range, so
+						 * consider it matching
+						 */
+						matches = true;
+
+						break;
+					}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		val = ranges->values[2 * ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+
+	/* MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS(); */
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges *serialized_a;
+	SerializedRanges *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	ExpandedRange *eranges;
+	int			neranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue *distances;
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a);
+	ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	neranges = (ranges_a->nranges + ranges_a->nvalues) +
+		(ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges with entries for the first range */
+	fill_expanded_ranges(eranges, ranges_a->nranges + ranges_a->nvalues,
+						 ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_expanded_ranges(&eranges[ranges_a->nranges + ranges_a->nvalues],
+						 ranges_b->nranges + ranges_b->nvalues,
+						 ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* sort the expanded ranges */
+	sort_expanded_ranges(cmpFn, colloid, eranges, neranges);
+
+	/*
+	 * We've loaded two different lists of expanded ranges, so some of them
+	 * may be overlapping. So walk through them and merge them.
+	 */
+	neranges = merge_overlapping_ranges(cmpFn, colloid, eranges, neranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidExpandedRanges(bdesc, colloid, attno, attr, eranges, neranges);
+
+	/*
+	 * If needed, reduce some of the ranges.
+	 *
+	 * XXX This may be fairly expensive, so maybe we should do it only when
+	 * it's actually needed (when we have too many ranges).
+	 */
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many of them to get below the threshold. The
+	 * collapsed ranges will be stored as a single value.
+	 *
+	 * XXX This does not apply the load factor, as we don't expect to add more
+	 * values to the range, so we prefer to keep as many ranges as possible.
+	 *
+	 * XXX Can the maxvalues be different in the two ranges? Perhaps we should
+	 * use maximum of those?
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  ranges_a->maxvalues,
+									  cmpFn, colloid);
+
+	/* update the first range summary */
+	store_expanded_ranges(ranges_a, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+								   uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 8, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges	   *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges->maxvalues, ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum		a,
+					b;
+		text	   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum		a;
+		text	   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..69ce6dfca4 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(brdesc,
+												  tuple->bt_columns[keyno].bv_mem_value,
+												  tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -169,15 +177,15 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
 		{
-			Datum value = tuple->bt_columns[keyno].bv_values[datumno];
+			Datum		value = tuple->bt_columns[keyno].bv_values[datumno];
 
 #ifdef TOAST_INDEX_HACK
 
 			/* We must look at the stored type, not at the index descriptor. */
-			TypeCacheEntry	*atttype = brdesc->bd_info[keyno]->oi_typcache[datumno];
+			TypeCacheEntry *atttype = brdesc->bd_info[keyno]->oi_typcache[datumno];
 
 			/* Do we need to free the value at the end? */
-			bool free_value = false;
+			bool		free_value = false;
 
 			/* For non-varlena types we don't need to do anything special */
 			if (atttype->typlen != -1)
@@ -193,9 +201,9 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 * If value is stored EXTERNAL, must fetch it so we are not
 			 * depending on outside storage.
 			 *
-			 * XXX Is this actually true? Could it be that the summary is
-			 * NULL even for range with non-NULL data? E.g. degenerate bloom
-			 * filter may be thrown away, etc.
+			 * XXX Is this actually true? Could it be that the summary is NULL
+			 * even for range with non-NULL data? E.g. degenerate bloom filter
+			 * may be thrown away, etc.
 			 */
 			if (VARATT_IS_EXTERNAL(DatumGetPointer(value)))
 			{
@@ -205,8 +213,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			}
 
 			/*
-			 * If value is above size target, and is of a compressible datatype,
-			 * try to compress it in-line.
+			 * If value is above size target, and is of a compressible
+			 * datatype, try to compress it in-line.
 			 */
 			if (!VARATT_IS_EXTENDED(DatumGetPointer(value)) &&
 				VARSIZE(DatumGetPointer(value)) > TOAST_INDEX_TARGET &&
@@ -495,6 +503,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +587,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 8cc4e532e6..89254b5766 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for minmax-multi indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..87de94f397 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,11 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+/*
+ * The BRIN opclasses may register serialization callback, in case the on-disk
+ * and in-memory representations differ (e.g. for performance reasons).
+ */
+typedef void (*brin_serialize_callback_type) (BrinDesc *bdesc, Datum src, Datum *dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +32,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 82e874130d..654584a03f 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 04d678f96a..8135854163 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2062,6 +2208,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2088,6 +2251,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2155,6 +2334,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2180,6 +2425,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2202,6 +2464,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2224,6 +2503,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2288,6 +2584,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2439,6 +2752,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2470,6 +2929,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2492,6 +2968,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2548,6 +3041,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2570,6 +3080,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2636,6 +3163,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 6709c8dfea..51403716b1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index dea9adaf98..ba9231ac8c 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9926',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '9901',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9927',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '9902',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9928',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '9904',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9929',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '9905',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9930',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '9909',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '9910',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9931',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9932',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '9911',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9933',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '9912',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9934',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '9913',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9935',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '9914',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9936',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '9916',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9937',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '9917',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9938',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '9918',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9939',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '9919',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 36c1433379..8baafdd7c6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8212,6 +8212,77 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9940', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9941', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9942', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9943', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9944', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9945', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9946', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9947', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9948', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9949', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9950', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9951', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9952', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9953', descr => 'BRIN multi minmax date distance',
+  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
+{ oid => '9954', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9955', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9956', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9957', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9958', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9959', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9960', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+{ oid => '9961', descr => 'BRIN multi minmax timestamp distance',
+  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11436,4 +11507,18 @@
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
 
+{ oid => '9962', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9963', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9964', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9965', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 74e279cbf9..e809094490 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -685,4 +685,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+{ oid => '9966',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..e13cb59c7e
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,445 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+ERROR:  value 7 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+DROP INDEX brinidx_multi;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index e568b9fea2..0541c12a25 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9925 | pg_brin_bloom_summary
-(5 rows)
+ 9966 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ecd0806718..d34fc7ef88 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index c0d7fa76f1..91834a984e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..6d61fb84c6
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,397 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+
+DROP INDEX brinidx_multi;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0008-Ignore-correlation-for-new-BRIN-opclasses-20210308.patchtext/x-patch; charset=UTF-8; name=0008-Ignore-correlation-for-new-BRIN-opclasses-20210308.patchDownload
From 927688cbf772c5a0ea980a0b7c2e6cdbbc81f891 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 8/8] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index c086e83236..22590b2351 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -412,6 +412,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index d2dc38adc8..6b1dd1040c 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1737,6 +1737,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 52314d3aa1..0320d128f6 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 89254b5766..063b703208 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

#157Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#156)
8 attachment(s)
Re: WIP: BRIN multi-range indexes

Minor fix, adding the bsearch_arg to Mkvcbuild.pm (per cfbot failure).

regards

On 3/8/21 1:53 AM, Tomas Vondra wrote:

Hi,

Here is a slightly improved patch series, fixing a lot of wording issues
and typos (thanks to Justin Pryzby). I also realized create_index.sgml
is not the right place to document opclass parameters - that worked when
the parameters were speficified in WITH, but that's no longer the case.

So I've moved this bit to brin.sgml, under the table listing opclasses.

On 3/5/21 1:37 AM, Tomas Vondra wrote:

Hi,

Here is an updated version of the patch series, with a couple minor
changes/improvements.

1) adding bsearch_arg to src/port/

2) moving minmax/inclusion changes from 0002 to a separate patch 0003

I think we should either ditch the 0003 (i.e. keep the existing
opclasses unchanged) or commit 0003 (in which case I'd vote to just stop
supporting the old signature of the consistent function).

Still not sure what do to about this. I'm leaning towards keeping 0003
and just removing the "old" signature entirely, to keep the API cleaner.
It might cause some breakage in out-of-core BRIN opclasses, but that
seems like a reasonable price. Moreover, the opclasses may need some
updating anyway, because of the changes in handling NULL scan keys (0004
moves that from the opclass to the bringetbitmap function).

The remaining part that didn't get much review is the very last patch,
adding an option to ignore correlation for some BRIN opclases. This is
needed as the regular BRIN costing is quite sensitive to correlation,
and the cost gets way too high for poorly correlated data, making it
unlikely the index will be used. But handling such data sets efficiently
is the main point of those new opclasses. Any opinions on this?

Not sure about this.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-introduce-bsearch_arg-20210308b.patchtext/x-patch; charset=UTF-8; name=0001-introduce-bsearch_arg-20210308b.patchDownload
From 74bc91d94bc5991d631a17013e6ad4e9f83344ac Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 5 Mar 2021 00:16:40 +0100
Subject: [PATCH 1/8] introduce bsearch_arg

---
 src/backend/statistics/extended_stats.c       | 31 --------------
 src/include/port.h                            |  5 +++
 .../statistics/extended_stats_internal.h      |  5 ---
 src/port/Makefile                             |  1 +
 src/port/bsearch_arg.c                        | 40 +++++++++++++++++++
 src/tools/msvc/Mkvcbuild.pm                   |  2 +-
 6 files changed, 47 insertions(+), 37 deletions(-)
 create mode 100644 src/port/bsearch_arg.c

diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index a030ea3653..fa42851fd5 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -659,37 +659,6 @@ compare_datums_simple(Datum a, Datum b, SortSupport ssup)
 	return ApplySortComparator(a, false, b, false, ssup);
 }
 
-/* simple counterpart to qsort_arg */
-void *
-bsearch_arg(const void *key, const void *base, size_t nmemb, size_t size,
-			int (*compar) (const void *, const void *, void *),
-			void *arg)
-{
-	size_t		l,
-				u,
-				idx;
-	const void *p;
-	int			comparison;
-
-	l = 0;
-	u = nmemb;
-	while (l < u)
-	{
-		idx = (l + u) / 2;
-		p = (void *) (((const char *) base) + (idx * size));
-		comparison = (*compar) (key, p, arg);
-
-		if (comparison < 0)
-			u = idx;
-		else if (comparison > 0)
-			l = idx + 1;
-		else
-			return (void *) p;
-	}
-
-	return NULL;
-}
-
 /*
  * build_attnums_array
  *		Transforms a bitmap into an array of AttrNumber values.
diff --git a/src/include/port.h b/src/include/port.h
index 227ef4b148..82f63de325 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -508,6 +508,11 @@ typedef int (*qsort_arg_comparator) (const void *a, const void *b, void *arg);
 extern void qsort_arg(void *base, size_t nel, size_t elsize,
 					  qsort_arg_comparator cmp, void *arg);
 
+extern void *bsearch_arg(const void *key, const void *base,
+						 size_t nmemb, size_t size,
+						 int (*compar) (const void *, const void *, void *),
+						 void *arg);
+
 /* port/chklocale.c */
 extern int	pg_get_encoding_from_locale(const char *ctype, bool write_message);
 
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index c849bd57c0..a0a3cf5b0f 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -85,11 +85,6 @@ extern int	multi_sort_compare_dims(int start, int end, const SortItem *a,
 extern int	compare_scalars_simple(const void *a, const void *b, void *arg);
 extern int	compare_datums_simple(Datum a, Datum b, SortSupport ssup);
 
-extern void *bsearch_arg(const void *key, const void *base,
-						 size_t nmemb, size_t size,
-						 int (*compar) (const void *, const void *, void *),
-						 void *arg);
-
 extern AttrNumber *build_attnums_array(Bitmapset *attrs, int *numattrs);
 
 extern SortItem *build_sorted_items(int numrows, int *nitems, HeapTuple *rows,
diff --git a/src/port/Makefile b/src/port/Makefile
index e41b005c4f..52dbf5783f 100644
--- a/src/port/Makefile
+++ b/src/port/Makefile
@@ -40,6 +40,7 @@ LIBS += $(PTHREAD_LIBS)
 OBJS = \
 	$(LIBOBJS) \
 	$(PG_CRC32C_OBJS) \
+	bsearch_arg.o \
 	chklocale.o \
 	erand48.o \
 	inet_net_ntop.o \
diff --git a/src/port/bsearch_arg.c b/src/port/bsearch_arg.c
new file mode 100644
index 0000000000..d24dc4b7c4
--- /dev/null
+++ b/src/port/bsearch_arg.c
@@ -0,0 +1,40 @@
+/*
+ *	bsearch_arg.c: bsearch variant with a user-supplied pointer
+ *
+ *	src/port/bsearch_arg.c
+ */
+
+
+#include "c.h"
+
+
+/* simple counterpart to qsort_arg */
+void *
+bsearch_arg(const void *key, const void *base, size_t nmemb, size_t size,
+			int (*compar) (const void *, const void *, void *),
+			void *arg)
+{
+	size_t		l,
+				u,
+				idx;
+	const void *p;
+	int			comparison;
+
+	l = 0;
+	u = nmemb;
+	while (l < u)
+	{
+		idx = (l + u) / 2;
+		p = (void *) (((const char *) base) + (idx * size));
+		comparison = (*compar) (key, p, arg);
+
+		if (comparison < 0)
+			u = idx;
+		else if (comparison > 0)
+			l = idx + 1;
+		else
+			return (void *) p;
+	}
+
+	return NULL;
+}
diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm
index 49614106dc..20221c1ae3 100644
--- a/src/tools/msvc/Mkvcbuild.pm
+++ b/src/tools/msvc/Mkvcbuild.pm
@@ -101,7 +101,7 @@ sub mkvcbuild
 	  dirent.c dlopen.c getopt.c getopt_long.c link.c
 	  pread.c preadv.c pwrite.c pwritev.c pg_bitutils.c
 	  pg_strong_random.c pgcheckdir.c pgmkdirp.c pgsleep.c pgstrcasecmp.c
-	  pqsignal.c mkdtemp.c qsort.c qsort_arg.c quotes.c system.c
+	  pqsignal.c mkdtemp.c qsort.c qsort_arg.c bsearch_arg.c quotes.c system.c
 	  strerror.c tar.c thread.c
 	  win32env.c win32error.c win32security.c win32setlocale.c win32stat.c);
 
-- 
2.26.2

0002-Pass-all-scan-keys-to-BRIN-consistent-func-20210308b.patchtext/x-patch; charset=UTF-8; name=0002-Pass-all-scan-keys-to-BRIN-consistent-func-20210308b.patchDownload
From 6d583a498501e860275d03dbce9b152f379b2e58 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 2/8] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

The code continues to support both the original (one scan key at a time)
and new (all scan keys at once) approaches, depending on whether the
consistent function accepts three or four arguments.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c          | 158 +++++++++++++++++++-----
 src/backend/access/brin/brin_validate.c |   4 +-
 2 files changed, 126 insertions(+), 36 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 27ba596c6e..f9a0476024 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -390,6 +390,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey   **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -411,6 +414,66 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure. We allocate space for all attributes, so
+	 * that we don't have to bother determining which attributes are used.
+	 *
+	 * XXX The widest table can have ~1600 attributes, so this may allocate a
+	 * couple kilobytes of memory). We could invent a more compact approach
+	 * (with just space for used attributes) but that would make the matching
+	 * more complicated, so it may not be a win.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation used in the
+		 * index column (but only if the search is not IS NULL/ IS NOT NULL).
+		 * Otherwise we shouldn't be using this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno - 1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many scan keys
+			 * are there for this attribute, so we simply allocate the largest
+			 * number possible. This may waste a bit of memory, but we only
+			 * expect small number of scan keys in general, so this should be
+			 * negligible, and it's cheaper than having to repalloc
+			 * repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -471,7 +534,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int			attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -481,51 +544,78 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
+					/* skip attributes without any scan keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
 
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
+					bval = &dtup->bt_columns[attno - 1];
 
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * The opclass may or may not support processing of
+					 * multiple scan keys. We can determine that based on the
+					 * number of arguments - functions with extra parameter
+					 * (number of scan keys) do support this, otherwise we
+					 * have to simply pass the scan keys one by one, as
+					 * before.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
-											PointerGetDatum(bdesc),
-											PointerGetDatum(bval),
-											PointerGetDatum(key));
-					addrange = DatumGetBool(add);
+					if (consistentFn[attno - 1].fn_nargs >= 4)
+					{
+						Oid			collation;
+
+						/*
+						 * Collation from the first key (has to be the same
+						 * for all keys for the same attribue).
+						 */
+						collation = keys[attno - 1][0]->sk_collation;
+
+						/* Check all keys at once */
+						add = FunctionCall4Coll(&consistentFn[attno - 1],
+												collation,
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												PointerGetDatum(keys[attno - 1]),
+												Int32GetDatum(nkeys[attno - 1]));
+						addrange = DatumGetBool(add);
+					}
+					else
+					{
+						/*
+						 * Check keys one by one
+						 *
+						 * When there are multiple scan keys, failure to meet
+						 * the criteria for a single one of them is enough to
+						 * discard the range as a whole, so break out of the
+						 * loop as soon as a false return value is obtained.
+						 */
+						int			keyno;
+
+						for (keyno = 0; keyno < nkeys[attno - 1]; keyno++)
+						{
+							add = FunctionCall3Coll(&consistentFn[attno - 1],
+													keys[attno - 1][keyno]->sk_collation,
+													PointerGetDatum(bdesc),
+													PointerGetDatum(bval),
+													PointerGetDatum(keys[attno - 1][keyno]));
+							addrange = DatumGetBool(add);
+
+							/* mismatching key, no need to look further  */
+							if (!addrange)
+								break;
+						}
+					}
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..11835d85cd 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											3, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-- 
2.26.2

0003-Process-all-scan-keys-in-existing-BRIN-opc-20210308b.patchtext/x-patch; charset=UTF-8; name=0003-Process-all-scan-keys-in-existing-BRIN-opc-20210308b.patchDownload
From f6d1ebb0635d590a17eed62376e95f90679e3aa5 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 5 Mar 2021 00:45:33 +0100
Subject: [PATCH 3/8] Process all scan keys in existing BRIN opclasses

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow more advanced opclases in the future.
---
 src/backend/access/brin/brin_inclusion.c | 140 ++++++++++++++++-------
 src/backend/access/brin/brin_minmax.c    |  92 ++++++++++++---
 src/include/catalog/pg_proc.dat          |   4 +-
 3 files changed, 177 insertions(+), 59 deletions(-)

diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..a260074c91 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -251,6 +253,10 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 /*
  * BRIN inclusion consistent function
  *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches all those keys, we check the regular scan keys.
+ *
  * All of the strategies are optional.
  */
 Datum
@@ -258,24 +264,31 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/* Handle IS NULL/IS NOT NULL tests */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -284,7 +297,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -293,7 +311,14 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
@@ -301,10 +326,45 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all regular scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
+	/* This should be called only for regular keys, not for IS [NOT] NULL. */
+	Assert(!(key->sk_flags & SK_ISNULL));
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +384,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +444,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..e116084a02 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -140,29 +142,41 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's min/max
  * values.  Return true if so, false otherwise.
+ *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches all those keys, we check the regular scan keys.
  */
 Datum
 brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
 	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -171,7 +185,12 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -180,13 +199,52 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
+	}
+
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
+
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +287,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 506689d8ac..ad11a2b66f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8206,7 +8206,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8222,7 +8222,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0004-Move-IS-NOT-NULL-handling-from-BRIN-suppor-20210308b.patchtext/x-patch; charset=UTF-8; name=0004-Move-IS-NOT-NULL-handling-from-BRIN-suppor-20210308b.patchDownload
From 2532cb8b5c0f655894d9ad685a494380514850b9 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 2 Mar 2021 19:27:48 +0100
Subject: [PATCH 4/8] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c |  96 +-------
 src/backend/access/brin/brin_minmax.c    |  93 +------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 244 insertions(+), 241 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index f9a0476024..9f2656b8d9 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+								BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -179,7 +182,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -243,31 +245,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -390,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey   **keys;
-	int		   *nkeys;
+	ScanKey   **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -419,13 +399,18 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * consistent support procedure. We allocate space for all attributes, so
 	 * that we don't have to bother determining which attributes are used.
 	 *
+	 * We keep null and regular keys separate, so that we can pass just the
+	 * regular keys to the consistent function easily.
+	 *
 	 * XXX The widest table can have ~1600 attributes, so this may allocate a
 	 * couple kilobytes of memory). We could invent a more compact approach
 	 * (with just space for used attributes) but that would make the matching
 	 * more complicated, so it may not be a win.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -445,23 +430,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno - 1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan keys are
+		 * there for a given attribute, so we simply allocate the largest
+		 * number possible (as if all scan keys belonged to the same
+		 * attribute). This may waste a bit of memory, but we only expect
+		 * small number of scan keys in general, so this should be negligible,
+		 * and it's probably cheaper than having to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many scan keys
-			 * are there for this attribute, so we simply allocate the largest
-			 * number possible. This may waste a bit of memory, but we only
-			 * expect small number of scan keys in general, so this should be
-			 * negligible, and it's cheaper than having to repalloc
-			 * repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -469,9 +454,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -549,15 +548,58 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval;
 					Datum		add;
 
-					/* skip attributes without any scan keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular and
+					 * IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine
+					 * mismatch within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all
+					 * passed. If there are no regular scan keys, we're done -
+					 * the page range matches. If there are regular keys, but
+					 * the page range is marked as 'all nulls' it can't
+					 * possibly pass (we're assuming the operators are
+					 * strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -703,7 +745,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -732,25 +773,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1529,6 +1553,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1582,3 +1639,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values; our
+	 * deformed tuple will get updated if the new tuple doesn't fit the
+	 * original range (note this means we can't break out of the loop early).
+	 * Make a note of whether this happens, so that we know to insert the
+	 * modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's the
+			 * first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index a260074c91..b17077703c 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -268,52 +258,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* Handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
 
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
-
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -331,9 +278,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -574,37 +520,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index e116084a02..330bed0487 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -156,52 +146,9 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
 
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -215,9 +162,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -307,34 +253,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 78c89a6961..79440ebe7b 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0005-Optimize-allocations-in-bringetbitmap-20210308b.patchtext/x-patch; charset=UTF-8; name=0005-Optimize-allocations-in-bringetbitmap-20210308b.patchDownload
From e2af14e512b404fa52ab9623e8d05ab4588f938d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 2 Mar 2021 19:57:27 +0100
Subject: [PATCH 5/8] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 60 ++++++++++++++++++++++++++--------
 1 file changed, 47 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 9f2656b8d9..1f82e965f9 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -402,15 +405,52 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * We keep null and regular keys separate, so that we can pass just the
 	 * regular keys to the consistent function easily.
 	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly the
+	 * same lifetime, so that's OK.
+	 *
 	 * XXX The widest table can have ~1600 attributes, so this may allocate a
 	 * couple kilobytes of memory). We could invent a more compact approach
 	 * (with just space for used attributes) but that would make the matching
 	 * more complicated, so it may not be a win.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* regular keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* NULL keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -444,9 +484,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -457,17 +497,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0006-BRIN-bloom-indexes-20210308b.patchtext/x-patch; charset=UTF-8; name=0006-BRIN-bloom-indexes-20210308b.patchDownload
From cea900daa99eeb93bf6755eb891667b95439e51f Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 6/8] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 230 ++++++-
 doc/src/sgml/ref/create_index.sgml        |   1 +
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 787 ++++++++++++++++++++++
 src/include/access/brin.h                 |   2 +
 src/include/access/brin_internal.h        |   4 +
 src/include/catalog/pg_amop.dat           | 116 ++++
 src/include/catalog/pg_amproc.dat         | 447 ++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 ++
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +-
 src/test/regress/expected/brin_bloom.out  | 428 ++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 376 +++++++++++
 19 files changed, 2555 insertions(+), 7 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 06880c0f7b..58126a5a3c 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -115,7 +115,8 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
   operator classes store the minimum and the maximum values appearing
   in the indexed column within the range.  The <firstterm>inclusion</firstterm>
   operator classes store a value which includes the values in the indexed
-  column within the range.
+  column within the range.  The <firstterm>bloom</firstterm> operator
+  classes build a Bloom filter for all values in the range.
  </para>
 
  <table id="brin-builtin-opclasses-table">
@@ -128,6 +129,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +159,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +173,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +187,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +201,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +215,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +229,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +253,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +267,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +281,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +295,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +309,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +323,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +337,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +351,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +365,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +379,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +393,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +425,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +439,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +453,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +467,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +481,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +495,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +509,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -409,6 +534,55 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    </tbody>
   </tgroup>
  </table>
+
+  <sect2 id="brin-builtin-opclasses--parameters">
+   <title>Operator Class Parameters</title>
+
+   <para>
+    Some of the built-in operator classes allow specifying parameters affecting
+    behavior of the operator class.  Each operator class has its own set of
+    allowed parameters.  Only the <literal>bloom</literal> operator class
+    allows specifying parameters:
+   </para>
+
+   <para>
+    <acronym>bloom</acronym> operator classes accept these parameters:
+   </para>
+
+   <variablelist>
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default value is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be
+     between 0.0001 and 0.25. The default value is 0.01, which is 1% false
+     positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   </variablelist>
+  </sect2>
+
 </sect1>
 
 <sect1 id="brin-extensibility">
@@ -779,6 +953,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 51b4d57939..4a29935785 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -581,6 +581,7 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..4494484b3b
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,787 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test whether it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_page.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001	/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25	/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+
+#define BloomMaxFilterSize \
+	MAXALIGN_DOWN(BLCKSZ - \
+				  (MAXALIGN(SizeOfPageHeaderData + \
+							sizeof(ItemIdData)) + \
+				   MAXALIGN(sizeof(BrinSpecialSpace)) + \
+				   SizeOfBrinTuple))
+
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * To calculate the additional hashes (treating the uint32 hash as an input
+ * value), we use the approach with two hash functions from this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions are calculated using hard-coded seeds.
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes
+ * that are not entirely 0. But while indexes don't support TOAST, the
+ * varlena can still be compressed. So this seems unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high. But even if the false positive rate
+ * exceeds the desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16		flags;
+
+	/* fields for the HASHED phase */
+	uint8		nhashes;		/* number of hash functions */
+	uint32		nbits;			/* number of bits in the bitmap (size) */
+	uint32		nbits_set;		/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+
+}			BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size		len;
+	BloomFilter *filter;
+
+	int			nbits;			/* size of filter / number of bits */
+	int			nbytes;			/* size of filter / number of bytes */
+
+	double		k;				/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	nbits = ceil(-(ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	nbytes = ((nbits + 7) / 8);
+	nbits = nbytes * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * Initially the bloom filter is just zeroes and so very compressible, but
+	 * as we add values it gets more and more random, and so less and less
+	 * compressible. So initially everything fits on the page, but we might
+	 * get surprising failures later - we want to prevent that, so we reject
+	 * bloom filter that are obviously too large.
+	 *
+	 * XXX It's not uncommon to oversize the bloom filter a bit, to defend
+	 * against unexpected data anomalies (parts of table with more distinct
+	 * values per range etc.). But we still need to make sure even the
+	 * oversized filter fits on page, if such need arises.
+	 *
+	 * XXX This check is not perfect, because the index may have multiple
+	 * filters that are small individually, but too large when combined.
+	 */
+	if (nbytes > BloomMaxFilterSize)
+		elog(ERROR, "the bloom filter is too large (%d > %zu)", nbytes,
+			 BloomMaxFilterSize);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * nbits / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so the
+	 * varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + nbytes;
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = nbits;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter * filter, uint32 value, bool *updated)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (!(filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter * filter, uint32 value)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* calculate the two hashes */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (!(filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash), but
+	 * let's keep the array just like inclusion and minman opclasses, for
+	 * consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+}			BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+									uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double		ndistinct;
+	double		maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this case to
+	 * maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a couple of
+	 * safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples, in the
+	 * range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from
+	 * BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to
+				 * return the current page range if the minimum value in the
+				 * range <= scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters for now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing text
+	 * input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 79440ebe7b..8cc4e532e6 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..04d678f96a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,20 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2040,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2062,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2084,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2155,14 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2180,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2202,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2224,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2266,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2288,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2439,20 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2470,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2492,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2548,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2570,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2636,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..6709c8dfea 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..dea9adaf98 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9901',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '9902',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '9903',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9904',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9905',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '9906',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '9907',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '9908',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9909',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9910',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9911',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9912',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9913',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '9914',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '9915',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9916',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9917',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9918',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9919',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ad11a2b66f..36c1433379 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8229,6 +8229,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '9920', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '9921', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '9922', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '9923', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '9924', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11402,4 +11422,18 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8959c2f53b..74e279cbf9 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,5 +679,10 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-
+{ oid => '9925',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..32c56a996a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,428 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..e568b9fea2 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9925 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index c77b0d7342..ecd0806718 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 0264a97324..c0d7fa76f1 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..5d499208e3
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,376 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0007-BRIN-minmax-multi-indexes-20210308b.patchtext/x-patch; charset=UTF-8; name=0007-BRIN-minmax-multi-indexes-20210308b.patchDownload
From 0f25c8c40ea7695146a0271b1b190526ef87d749 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Wed, 3 Feb 2021 19:00:00 +0100
Subject: [PATCH 7/8] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  276 +-
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_bloom.c        |    4 +-
 src/backend/access/brin/brin_minmax_multi.c | 2982 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   33 +-
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    8 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 ++++
 src/include/catalog/pg_amproc.dat           |  600 +++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   85 +
 src/include/catalog/pg_type.dat             |    6 +
 src/test/regress/expected/brin_multi.out    |  445 +++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  397 +++
 21 files changed, 5465 insertions(+), 30 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 58126a5a3c..1fbdcbeee4 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -116,7 +116,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
   in the indexed column within the range.  The <firstterm>inclusion</firstterm>
   operator classes store a value which includes the values in the indexed
   column within the range.  The <firstterm>bloom</firstterm> operator
-  classes build a Bloom filter for all values in the range.
+  classes build a Bloom filter for all values in the range.  The
+  <firstterm>minmax-multi</firstterm> operator classes store multiple
+  minimum and maximum values, representing values appearing in the indexed
+  column within the range.
  </para>
 
  <table id="brin-builtin-opclasses-table">
@@ -215,6 +218,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -229,6 +241,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -243,6 +264,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -267,6 +297,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -281,6 +320,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -295,6 +343,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -309,6 +366,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -323,6 +389,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -337,6 +412,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -351,6 +435,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -379,6 +472,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -393,6 +495,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -407,6 +518,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -453,6 +573,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -467,6 +596,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -481,6 +619,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -495,6 +642,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -509,6 +665,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -523,6 +688,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -581,6 +755,25 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    </varlistentry>
 
    </variablelist>
+
+   <para>
+    <acronym>minmax-multi</acronym> operator classes accept these parameters:
+   </para>
+
+   <variablelist>
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     either a point, or a boundary of an interval. Values must be between
+     8 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   </variablelist>
   </sect2>
 
 </sect1>
@@ -704,13 +897,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, minmax-multi, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -1007,6 +1201,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The minmax-multi operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, minmax-multi allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the minmax-multi support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-minmax-multi-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-minmax-multi-table">
+  <title>Procedure and Support Numbers for minmax-multi Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 4494484b3b..c086e83236 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -83,7 +83,7 @@
  * the whole index tuple that has to fit into a page. And for multi-column
  * indexes that may include pieces we have no control over (not necessarily
  * bloom filters, the other columns may use other BRIN opclasses). So it's
- * not entirely clear how to distrubute the space between those columns.
+ * not entirely clear how to distribute the space between those columns.
  *
  * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
  * make some basic sizing decisions, based on the size of BRIN ranges, and
@@ -468,7 +468,7 @@ brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
 
 	/*
 	 * Positive values are to be used directly, but we still apply a couple of
-	 * safeties no to use unreasonably small bloom filters.
+	 * safeties to avoid using unreasonably small bloom filters.
 	 */
 	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
 
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..d2dc38adc8
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2982 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With minmax-multi opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 50% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values */
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Sizing the insert buffer - we use 10x the number of values specified
+ * in the reloption, but we cap it to 8192 not to get too large. When
+ * the buffer gets full, we reduce the number of values by half.
+ */
+#define		MINMAX_BUFFER_FACTOR			10
+#define		MINMAX_BUFFER_MIN				256
+#define		MINMAX_BUFFER_MAX				8192
+#define		MINMAX_BUFFER_LOAD_FACTOR		0.5
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+}			MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange; /* number of values per range */
+}			MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0))
+
+/*
+ * The summary of minmax-multi indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* Cache information that we need quite often. */
+	Oid			typid;
+	Oid			colloid;
+	AttrNumber	attno;
+	FmgrInfo   *cmp;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nsorted;		/* number of sorted values (ranges + points) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/*
+	 * We simply add the values into a large buffer, without any expensive
+	 * steps (sorting, deduplication, ...). The buffer is a multiple of the
+	 * target number of values, so the compaction happen less often,
+	 * amortizing the costs. We keep the actual target and compact to the
+	 * requested number of values at the very end, before serializing to
+	 * on-disk representation.
+	 */
+	/* requested number of values */
+	int			target_maxvalues;
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum		values[FLEXIBLE_ARRAY_MEMBER];
+}			Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, with a simple header
+ * with basic metadata, followed by the boundary values. It has a varlena
+ * header, so can be treated as varlena directly.
+ *
+ * See range_serialize/range_deserialize for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* type of values stored in the data array */
+	Oid			typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+}			SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(int maxvalues, SerializedRanges *range);
+
+
+/*
+ * Used to represent ranges expanded to make merging and combining easier.
+ *
+ * Each expanded range is essentially an interval, represented by min/max
+ * values, along with a flag whether it's a collapsed range (in which case
+ * the min and max values are equal). We have the flag to handle by-ref
+ * data types - we can't simply compare the datums, and this saves some
+ * calls to the type-specific comparator function.
+ */
+typedef struct ExpandedRange
+{
+	Datum		minval;			/* lower boundary */
+	Datum		maxval;			/* upper boundary */
+	bool		collapsed;		/* true if minval==maxval */
+}			ExpandedRange;
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of extended ranges).
+ */
+typedef struct DistanceValue
+{
+	int			index;
+	double		value;
+}			DistanceValue;
+
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+										   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+													uint16 attno, Oid subtype,
+													uint16 strategynum);
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+}			compare_context;
+
+static int	compare_values(const void *a, const void *b, void *arg);
+
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+	int			i;
+	Datum		lt;
+
+	for (i = 0; i < (nvalues - 1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i + 1]);
+		Assert(DatumGetBool(lt));
+	}
+}
+#endif
+
+/*
+ * Comprehensive check of the Ranges structure.
+ */
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+
+	/* some basic sanity checks */
+	Assert(ranges->nranges >= 0);
+	Assert(ranges->nsorted >= 0);
+	Assert(ranges->nvalues >= ranges->nsorted);
+	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
+	Assert(ranges->typid != InvalidOid);
+
+	/*
+	 * First the ranges - there are 2*nranges boundary values, and the values
+	 * have to be strictly ordered (equal values would mean the range is
+	 * collapsed, and should be stored as a point). This also guarantees that
+	 * the ranges do not overlap.
+	 */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2 * ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2 * ranges->nranges],
+					 ranges->nsorted);
+
+	/*
+	 * Check that none of the values are not covered by ranges (both sorted
+	 * and unsorted)
+	 */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		compar;
+		int			start,
+					end;
+		Datum		minvalue,
+					maxvalue;
+
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nranges == 0)
+			break;
+
+		minvalue = ranges->values[0];
+		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+		/* smaller than the smallest value in the first range */
+		if (DatumGetBool(compar))
+			continue;
+
+		/*
+		 * Is the value greater than the maxval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+		/* larger than the largest value in the last range */
+		if (DatumGetBool(compar))
+			continue;
+
+		start = 0;				/* first range */
+		end = ranges->nranges - 1;	/* last range */
+		while (true)
+		{
+			int			midpoint = (start + end) / 2;
+
+			/* this means we ran out of ranges in the last step */
+			if (start > end)
+				break;
+
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2 * midpoint];
+			maxvalue = ranges->values[2 * midpoint + 1];
+
+			/*
+			 * Is the value smaller than the minval? If yes, we'll recurse to
+			 * the left side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+			{
+				end = (midpoint - 1);
+				continue;
+			}
+
+			/*
+			 * Is the value greater than the minval? If yes, we'll recurse to
+			 * the right side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+			{
+				start = (midpoint + 1);
+				continue;
+			}
+
+			/* hey, we found a matching range */
+			Assert(false);
+		}
+	}
+
+	/* and values in the unsorted part must not be in sorted part */
+	for (i = ranges->nsorted; i < ranges->nvalues; i++)
+	{
+		compare_context cxt;
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nsorted == 0)
+			break;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		Assert(bsearch_arg(&value, &ranges->values[2 * ranges->nranges],
+						   ranges->nsorted, sizeof(Datum),
+						   compare_values, (void *) &cxt) == NULL);
+	}
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidExpandedRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						  Form_pg_attribute attr, ExpandedRange *ranges,
+						  int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+	FmgrInfo   *eq;
+	FmgrInfo   *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum		r;
+		Datum		minval = ranges[i].minval;
+		Datum		maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else					/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e. upper <
+	 * lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges - 1; i++)
+	{
+		Datum		r;
+		Datum		maxval = ranges[i].maxval;
+		Datum		minval = ranges[i + 1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size		len;
+	Ranges	   *ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values); /* fixed header */
+	len += maxvalues * sizeof(Datum);	/* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+
+/*
+ * range_deduplicate_values
+ *		Deduplicate the part with values in the simple points.
+ *
+ * This is meant to be a cheaper way of reducing the size of the ranges. It
+ * does not touch the ranges, and only sorts the other values - it does not
+ * call the distance functions, which may be quite expensive, etc.
+ */
+static void
+range_deduplicate_values(Ranges *range)
+{
+	int			i,
+				n;
+	int			start;
+	compare_context cxt;
+
+	/*
+	 * If there are no unsorted values, we're done (this probably can't
+	 * happen, as we're adding values to unsorted part).
+	 */
+	if (range->nsorted == range->nvalues)
+		return;
+
+	/* sort the values */
+	cxt.colloid = range->colloid;
+	cxt.cmpFn = range->cmp;
+
+	/* how many values to sort? */
+	start = 2 * range->nranges;
+
+	qsort_arg(&range->values[start],
+			  range->nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	n = 1;
+	for (i = 1; i < range->nvalues; i++)
+	{
+		/* same as preceding value, so store it */
+		if (compare_values(&range->values[start + i - 1],
+						   &range->values[start + i],
+						   (void *) &cxt) == 0)
+			continue;
+
+		range->values[start + n] = range->values[start + i];
+
+		n++;
+	}
+
+	/* now all the values are sorted */
+	range->nvalues = n;
+	range->nsorted = n;
+
+	AssertCheckRanges(range, range->cmp, range->colloid);
+}
+
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size		len;
+	int			nvalues;
+	SerializedRanges *serialized;
+	Oid			typid;
+	int			typlen;
+	bool		typbyval;
+
+	int			i;
+	char	   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nsorted >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+	Assert(range->target_maxvalues > 0);
+
+	/* at this point the range should be compacted to the target size */
+	Assert(2 * range->nranges + range->nvalues <= range->target_maxvalues);
+
+	Assert(range->target_maxvalues <= range->maxvalues);
+
+	/* range boundaries are always sorted */
+	Assert(range->nvalues >= range->nsorted);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2 * range->nranges + range->nvalues;
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges, data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)			/* varlena */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)		/* cstring */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else						/* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->target_maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation this
+	 * depends on the particular data type).
+	 */
+	ptr = serialized->data;		/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int			tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int			tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + len));
+	}
+
+	/* exact size */
+	Assert(ptr == ((char *) serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(int maxvalues, SerializedRanges *serialized)
+{
+	int			i,
+				nvalues;
+	char	   *ptr;
+	bool		typbyval;
+	int			typlen;
+
+	Ranges	   *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2 * serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+	Assert(serialized->maxvalues <= maxvalues);
+
+	range = minmax_multi_init(maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->nsorted = serialized->nvalues;
+	range->maxvalues = maxvalues;
+	range->target_maxvalues = serialized->maxvalues;
+
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need to copy
+	 * the values and will instead just point the values to the serialized
+	 * varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *) serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+/*
+ * compare_expanded_ranges
+ *	  Compare the expanded ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_expanded_ranges(const void *a, const void *b, void *arg)
+{
+	ExpandedRange *ra = (ExpandedRange *) a;
+	ExpandedRange *rb = (ExpandedRange *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * compare_values
+ *	  Compare the values.
+ */
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum	   *da = (Datum *) a;
+	Datum	   *db = (Datum *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *da, *db);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *db, *da);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Check if the new value matches one of the existing ranges.
+ */
+static bool
+has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges,
+				   Datum newval, AttrNumber attno, Oid typid)
+{
+	Datum		compar;
+
+	Datum		minvalue = ranges->values[0];
+	Datum		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+
+	/* binary search on ranges */
+	int			start,
+				end;
+
+	if (ranges->nranges == 0)
+		return false;
+
+	/*
+	 * Otherwise, need to compare the new value with boundaries of all the
+	 * ranges. First check if it's less than the absolute minimum, which is
+	 * the first value in the array.
+	 */
+	cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												   BTLessStrategyNumber);
+	compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+	/* smaller than the smallest value in the range list */
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * And now compare it to the existing maximum (last value in the data
+	 * array). But only if we haven't already ruled out a possible match in
+	 * the minvalue check.
+	 */
+	cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													  BTGreaterStrategyNumber);
+	compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * So we know it's in the general min/max, the question is whether it
+	 * falls in one of the ranges or gaps. We'll do a binary search on
+	 * individual ranges - for each range we check equality (value falls
+	 * into the range), and then check ranges either above or below the
+	 * current range.
+	 */
+	start = 0;					/* first range */
+	end = (ranges->nranges - 1);	/* last range */
+	while (true)
+	{
+		int			midpoint = (start + end) / 2;
+
+		/* this means we ran out of ranges in the last step */
+		if (start > end)
+			return false;
+
+		/* copy the min/max values from the ranges */
+		minvalue = ranges->values[2 * midpoint];
+		maxvalue = ranges->values[2 * midpoint + 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in this range */
+		if (DatumGetBool(compar))
+		{
+			end = (midpoint - 1);
+			continue;
+		}
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+		/* larger than the largest value in this range */
+		if (DatumGetBool(compar))
+		{
+			start = (midpoint + 1);
+			continue;
+		}
+
+		/* hey, we found a matching range */
+		return true;
+	}
+
+	return false;
+}
+
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX We only inspect the sorted parts, which means we may add duplicate
+ * values. It may produce some false negatives when adding the values, but
+ * only after we already added some values (otherwise there is no unsorted
+ * part). And when querying the index, there should be no unsorted values,
+ * because the values are sorted and deduplicated during serialization.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+					 AttrNumber attno, Form_pg_attribute attr,
+					 Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid))
+		return true;
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													BTEqualStrategyNumber);
+
+	/*
+	 * There is no matching range, so let's inspect the sorted values.
+	 *
+	 * XXX We do a sequential search for small number of values, and binary
+	 * search once we have more than 16 values. This threshold is somewhat
+	 * arbitrary, as it depends on how expensive the comparison function is.
+	 * So maybe we should just do the binary search all the time.
+	 *
+	 * XXX If we use the threshold here, maybe we should do the same thing in
+	 * has_matching_range?
+	 */
+	if (ranges->nsorted >= 16)
+	{
+		compare_context cxt;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		if (bsearch_arg(&newval, &ranges->values[2 * ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) != NULL)
+			return true;
+	}
+	else
+	{
+		for (i = 2 * ranges->nranges; i < 2 * ranges->nranges + ranges->nsorted; i++)
+		{
+			Datum		compar;
+
+			compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+			/* found an exact match */
+			if (DatumGetBool(compar))
+				return true;
+		}
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * Expand ranges from Ranges into ExpandedRange array. This expects the
+ * eranges to be pre-allocated and with the correct size - there needs to be
+ * (nranges + nvalues) elements.
+ *
+ * The order of expanded ranges is arbitrary. We do expand the ranges first,
+ * and this part is sorted. But then we expand the values, and this part may
+ * be unsorted.
+ */
+static void
+fill_expanded_ranges(ExpandedRange *eranges, int neranges, Ranges *ranges)
+{
+	int			idx;
+	int			i;
+
+	/* Check that the output array has the right size. */
+	Assert(neranges == (2 * ranges->nranges + ranges->nvalues));
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * i];
+		eranges[idx].maxval = ranges->values[2 * i + 1];
+		eranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].maxval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	/* Did we produce the expected number of elements? */
+	Assert(idx == neranges);
+
+	return;
+}
+
+/*
+ * Sort and deduplicate expanded ranges.
+ *
+ * The ranges may be deduplicated - we're simply appending values, without
+ * checking for duplicates etc. So maybe the deduplication will reduce the
+ * number of ranges enough, and we won't have to compute the distances etc.
+ *
+ * Returns the number of expanded ranges.
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that some of the input data is already sorted (all the ranges
+ * and possibly some of the points) and do merge sort.
+ */
+static int
+sort_expanded_ranges(FmgrInfo *cmp, Oid colloid,
+					 ExpandedRange *eranges, int neranges)
+{
+	int			n;
+	int			i;
+	compare_context cxt;
+
+	Assert(neranges > 0);
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(eranges, neranges, sizeof(ExpandedRange),
+			  compare_expanded_ranges, (void *) &cxt);
+
+	/*
+	 * Deduplicate the ranges - simply compare each range to the preceding
+	 * one, and skip the duplicate ones.
+	 */
+	n = 1;
+	for (i = 1; i < neranges; i++)
+	{
+		/* if the current range is equal to the preceding one, do nothing */
+		if (!compare_expanded_ranges(&eranges[i - 1], &eranges[i], (void *) &cxt))
+			continue;
+
+		/* otherwise copy it to n-th place (if not already there) */
+		if (i != n)
+			memcpy(&eranges[n], &eranges[i], sizeof(ExpandedRange));
+
+		n++;
+	}
+
+	Assert((n > 0) && (n <= neranges));
+
+	return n;
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the expanded ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_overlapping_ranges(FmgrInfo *cmp, Oid colloid,
+						 ExpandedRange *eranges, int neranges)
+{
+	int			idx;
+
+	/* Merge ranges (idx) and (idx+1) if they overlap. */
+	idx = 0;
+	while (idx < (neranges - 1))
+	{
+		Datum		r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap if (minval
+		 * < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges are
+		 * ordered, so there are no more overlaps, because all the remaining
+		 * ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they overlap only partially.
+		 * So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].maxval);
+
+		if (DatumGetBool(r))
+			eranges[idx].maxval = eranges[idx + 1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectively of the
+		 * previous state).
+		 */
+		eranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the remaining
+		 * ranges by 1. There are ncranges elements, and we need to move
+		 * elements from (idx+2). That means the number of elements to move is
+		 * [ncranges - (idx+2)].
+		 */
+		memmove(&eranges[idx + 1], &eranges[idx + 2],
+				(neranges - (idx + 2)) * sizeof(ExpandedRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range, as
+		 * it might overlap with additional ranges thanks to the merge).
+		 */
+		neranges--;
+	}
+
+	return neranges;
+}
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ * This is intentionally sorting the distances in descending order, i.e.
+ * the longer gaps will be at the front.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *) a;
+	DistanceValue *db = (DistanceValue *) b;
+
+	if (da->value < db->value)
+		return 1;
+	else if (da->value > db->value)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Given an array of expanded ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (max-min) for pairs
+ * of consecutive ranges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_expanded_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			ndistances;
+	DistanceValue *distances;
+
+	Assert(neranges >= 2);
+
+	ndistances = (neranges - 1);
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * ndistances);
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges so
+	 * that we can sort them once.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		Datum		a1,
+					a2,
+					r;
+
+		a1 = eranges[i].maxval;
+		a2 = eranges[i + 1].minval;
+
+		/* compute length of the gap (between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index of the gap the distance is for */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/*
+	 * Sort the distances in descending order, so that the longest gaps are at
+	 * the front.
+	 */
+	pg_qsort(distances, ndistances, sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds expanded ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).  This expanded
+ * representation makes the processing a bit easier, as it allows handling
+ * ranges and points the same way.
+ *
+ * We sort and deduplicate the expanded ranges - this is necessary, because
+ * the points may be unsorted. And moreover the two parts (ranges and
+ * points) are sorted on their own.
+ */
+static ExpandedRange *
+build_expanded_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					  int *nranges)
+{
+	int			neranges;
+	ExpandedRange *eranges;
+
+	/* both ranges and points are expanded into a separate element */
+	neranges = ranges->nranges + ranges->nvalues;
+
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges */
+	fill_expanded_ranges(eranges, neranges, ranges);
+
+	/* sort and deduplicate the expanded ranges */
+	neranges = sort_expanded_ranges(cmp, colloid, eranges, neranges);
+
+	/* remember how many cranges we built */
+	*nranges = neranges;
+
+	return eranges;
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Counts boundary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(ExpandedRange *cranges, int ncranges)
+{
+	int			i;
+	int			count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+#endif
+
+/*
+ * reduce_expanded_ranges
+ *		reduce the ranges until the number of values is low enough
+ *
+ * Combines ranges until the number of boundary values drops below the
+ * threshold specified by max_values. This happens by merging enough
+ * ranges by distance between them.
+ *
+ * Returns the number of result ranges.
+ *
+ * We simply use the global min/max and then add boundaries for enough
+ * largest gaps. Each gap adds 2 values, so we simply use (target/2-1)
+ * distances. Then we simply sort all the values - each two values are
+ * a boundary of a range (possibly collapsed).
+ *
+ * XXX Some of the ranges may be collapsed (i.e. the min/max values are
+ * equal), but we ignore that for now. We could repeat the process,
+ * adding a couple more gaps recursively.
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ *
+ * XXX Or maybe we could just handle this by using random value as a
+ * tie-break, or by adding random noise to the actual distance.
+ */
+static int
+reduce_expanded_ranges(ExpandedRange *eranges, int neranges,
+					   DistanceValue *distances, int max_values,
+					   FmgrInfo *cmp, Oid colloid)
+{
+	int			i;
+	int			nvalues;
+	Datum	   *values;
+
+	compare_context cxt;
+
+	/* total number of gaps between ranges */
+	int			ndistances = (neranges - 1);
+
+	/* number of gaps to keep */
+	int			keep = (max_values / 2 - 1);
+
+	/*
+	 * Maybe we have sufficiently low number of ranges already?
+	 *
+	 * XXX This should happen before we actually do the expensive stuff like
+	 * sorting, so maybe this should be just an assert.
+	 */
+	if (keep >= ndistances)
+		return neranges;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	/* add the global min/max values, from the first/last range */
+	values[nvalues++] = eranges[0].minval;
+	values[nvalues++] = eranges[neranges - 1].maxval;
+
+	/* add boundary values for enough gaps */
+	for (i = 0; i < keep; i++)
+	{
+		/* index of the gap between (index) and (index+1) ranges */
+		int			index = distances[i].index;
+
+		Assert((index >= 0) && ((index + 1) < neranges));
+
+		/* add max from the preceding range, minval from the next one */
+		values[nvalues++] = eranges[index].maxval;
+		values[nvalues++] = eranges[index + 1].minval;
+
+		Assert(nvalues <= max_values);
+	}
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/*
+	 * Sort the values using the comparator function, and form ranges from the
+	 * sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		eranges[i].minval = values[2 * i];
+		eranges[i].maxval = values[2 * i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		eranges[i].collapsed = (compare_values(&values[2 * i],
+											   &values[2 * i + 1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+/*
+ * Store the boundary values from ExpandedRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_expanded_ranges(Ranges *ranges, ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (!eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->values[idx++] = eranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+
+	/* all the values are sorted */
+	ranges->nsorted = ranges->nvalues;
+
+	Assert(count_values(eranges, neranges) == 2 * ranges->nranges + ranges->nvalues);
+	Assert(2 * ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+}
+
+
+/*
+ * Consider freeing space in the ranges.
+ *
+ * Returns true if the value was actually modified.
+ */
+static bool
+ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *range)
+{
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	/*
+	 * If there is free space in the buffer, we're done without having to
+	 * modify anything.
+	 */
+	if (2 * range->nranges + range->nvalues < range->maxvalues)
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* did we reduce enough free space by just the deduplication? */
+	if (2 * range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR)
+		return true;
+
+	/*
+	 * We need to combine some of the existing ranges, to reduce the number of
+	 * values we have to store.
+	 *
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it (we might have
+	 * to do this repeatedly, even for a single BRIN page range). Otherwise
+	 * we'd have problems e.g. when building new indexes. So we use a memory
+	 * context and make sure we free the memory at the end (so if we call the
+	 * distance function many times, it might be an issue, but meh).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, colloid, range, &neranges);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * Combine ranges until we release at least 50% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR,
+									  cmpFn, colloid);
+
+	/* Make sure we've sufficiently reduced the number of ranges. */
+	Assert(count_values(eranges, neranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR);
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(range, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Did we break the ranges somehow? */
+	AssertCheckRanges(range, cmpFn, colloid);
+
+	return true;
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the minmax-multi range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn;
+	bool		modified = false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Make sure there's enough free space in the buffer. We only trigger this
+	 * when the buffer is full, which means it had to be modified as we size
+	 * it to be larger than what is stored on disk.
+	 *
+	 * XXX This needs to happen before we check if the value is contained in
+	 * the range, because the value might be in the unsorted part, and we
+	 * don't check that in range_contains_value. The deduplication would then
+	 * move it to the sorted part, and we'd add the value too, which violates
+	 * the rule that we never have duplicates with the ranges or sorted
+	 * values.
+	 *
+	 * XXX At the moment this only does the deduplication.
+	 *
+	 * XXX We might also deduplicate and recheck if the value is contained,
+	 * but that seems like an overkill. We'd need to deduplicate anyway, so
+	 * why not do it now.
+	 */
+	modified = ensure_free_space_in_buffer(bdesc, colloid,
+										   attno, attr, ranges);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then do the
+	 * deduplication in a batch, hoping for better efficiency. But that would
+	 * mean we actually modify the range every time, which means having to
+	 * serialize the value, which does palloc, walks the values, copies them,
+	 * etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming the
+	 * comparator function is not very expensive.
+	 *
+	 * This also implies the values array can't contain duplicate values.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return modified;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we do a
+	 * simple insertion sort. We could do something more elaborate, e.g. by
+	 * sorting the values only now and then, but for small counts (e.g. when
+	 * maxvalues is 64) this should be fine.
+	 */
+	ranges->values[2 * ranges->nranges + ranges->nvalues] = newval;
+	ranges->nvalues++;
+
+	/*
+	 * Check we haven't broken the ordering of boundary values (checks both
+	 * parts, but that doesn't hurt).
+	 */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/* Also check the range contains the value we just added. */
+	/* FIXME Assert(ranges, cmpFn, colloid); */
+
+	/* yep, we've modified the range */
+	return true;
+}
+
+/*
+ * Generate range representation of data collected during "batch mode".
+ * This is similar to reduce_expanded_ranges, except that we can't assume
+ * the values are sorted and there may be duplicate values.
+ */
+static void
+compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
+											   BTLessStrategyNumber);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, ranges->attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, ranges->colloid, ranges, &neranges);
+
+	/* FIXME this should do neranges >= max_values */
+	if (neranges > 1)
+	{
+		/* build array of gap distances and sort them in ascending order */
+		distances = build_distances(distanceFn, ranges->colloid,
+									eranges, neranges);
+
+		/*
+		 * Combine ranges until we get below max_values. We don't use any
+		 * scale factor, because this is used during serialization, and we
+		 * don't expect more tuples to be inserted anytime soon.
+		 */
+		neranges = reduce_expanded_ranges(eranges, neranges, distances,
+										  max_values, cmpFn, ranges->colloid);
+
+		Assert(count_values(eranges, neranges) <= max_values);
+	}
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(ranges, eranges, neranges);
+
+	/* check all the range invariants */
+	AssertCheckRanges(ranges, cmpFn, ranges->colloid);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float		a1 = PG_GETARG_FLOAT4(0);
+	float		a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double		a1 = PG_GETARG_FLOAT8(0);
+	double		a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16		a1 = PG_GETARG_INT16(0);
+	int16		a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32		a1 = PG_GETARG_INT32(0);
+	int32		a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64		a1 = PG_GETARG_INT64(0);
+	int64		a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double		da1,
+				da2;
+
+	ItemPointer pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	/*
+	 * We use the no-check variants here, because user-supplied values may
+	 * have (ip_posid == 0). See ItemPointerCompare.
+	 */
+	da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa1);
+
+	da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Computes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum		d;
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int			i;
+	float8		delta = 0;
+
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN - 1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute approximate distance between two dates.
+ */
+Datum
+brin_minmax_multi_distance_date(PG_FUNCTION_ARGS)
+{
+	DateADT		dateVal1 = PG_GETARG_DATEADT(0);
+	DateADT		dateVal2 = PG_GETARG_DATEADT(1);
+
+	if (DATE_NOT_FINITE(dateVal1) || DATE_NOT_FINITE(dateVal2))
+		PG_RETURN_FLOAT8(0);
+
+	PG_RETURN_FLOAT8(dateVal1 - dateVal2);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeADT		ta = PG_GETARG_TIMEADT(0);
+	TimeADT		tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+Datum
+brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Timestamp	dt1 = PG_GETARG_TIMESTAMP(0);
+	Timestamp	dt2 = PG_GETARG_TIMESTAMP(1);
+
+	if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
+		PG_RETURN_FLOAT8(0);
+
+	delta = dt2 - dt1;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Interval   *ia = PG_GETARG_INTERVAL_P(0);
+	Interval   *ib = PG_GETARG_INTERVAL_P(1);
+	Interval   *result;
+
+	result = (Interval *) palloc(sizeof(Interval));
+
+	result->month = ib->month - ia->month;
+	/* overflow check copied from int4mi */
+	if (!SAMESIGN(ib->month, ia->month) &&
+		!SAMESIGN(result->month, ib->month))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->day = ib->day - ia->day;
+	if (!SAMESIGN(ib->day, ia->day) &&
+		!SAMESIGN(result->day, ib->day))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->time = ib->time - ia->time;
+	if (!SAMESIGN(ib->time, ia->time) &&
+		!SAMESIGN(result->time, ib->time))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	/*
+	 * We assume months have 31 days - we don't need to be precise, in the
+	 * worst case we'll build somewhat less efficient ranges.
+	 */
+	delta = (float8) (result->month * 31 + result->day);
+
+	/* convert to microseconds (just like the time part) */
+	delta = 24L * 3600L * delta;
+
+	/* and add the time part */
+	delta += result->time / (float8) 1000000.0;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((float8) b->h - (float8) a->h);
+	delta /= 256;
+
+	delta += ((float8) b->g - (float8) a->g);
+	delta /= 256;
+
+	delta += ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from different families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+	int			i;
+	int			len;
+	unsigned char *addra,
+			   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be in
+	 * maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		PG_RETURN_FLOAT8(1.0);
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16;				/* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len - 1; i >= 0; i--)
+	{
+		delta += (float8) addrb[i] - (float8) addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
+{
+	Ranges	   *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s;
+
+	/*
+	 * In batch mode, we need to compress the accumulated values to the
+	 * actually requested number of values/ranges.
+	 */
+	compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+
+	s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges	   *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, if possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 *
+	 * When starting with an empty range, we assume this is a batch mode, i.e.
+	 * we size the buffer for the maximum possible number of items in the
+	 * range (based on range size and max number of items on a page).
+	 *
+	 * XXX This may require quite a bit of memory, so maybe we should use some
+	 * value in between. OTOH most tables will have much wider rows, so the
+	 * number of rows per page is much lower.
+	 *
+	 * XXX Maybe we should do this (using larger buffer) even when there
+	 * already is a summary?
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		int			target_maxvalues;
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		/* what was specified as a reloption? */
+		target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, target_maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+		ranges = minmax_multi_init(maxvalues);
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+		ranges->target_maxvalues = target_maxvalues;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, serialized->maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		ranges = range_deserialize(maxvalues, serialized);
+
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+
+	/* MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS(); */
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges	   *ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized->maxvalues, serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum		minval = ranges->values[2 * rangeno];
+		Datum		maxval = ranges->values[2 * rangeno + 1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+					{
+						Datum		compar;
+						FmgrInfo   *cmpFn;
+
+						/* by default this range does not match */
+						matches = false;
+
+						/*
+						 * Otherwise, need to compare the new value with
+						 * boundaries of all the ranges. First check if it's
+						 * less than the absolute minimum, which is the first
+						 * value in the array.
+						 */
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTLessStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+						/* smaller than the smallest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTGreaterStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+						/* larger than the largest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						/*
+						 * haven't managed to eliminate this range, so
+						 * consider it matching
+						 */
+						matches = true;
+
+						break;
+					}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		val = ranges->values[2 * ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+
+	/* MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS(); */
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges *serialized_a;
+	SerializedRanges *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	ExpandedRange *eranges;
+	int			neranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue *distances;
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a);
+	ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	neranges = (ranges_a->nranges + ranges_a->nvalues) +
+		(ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges with entries for the first range */
+	fill_expanded_ranges(eranges, ranges_a->nranges + ranges_a->nvalues,
+						 ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_expanded_ranges(&eranges[ranges_a->nranges + ranges_a->nvalues],
+						 ranges_b->nranges + ranges_b->nvalues,
+						 ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* sort the expanded ranges */
+	sort_expanded_ranges(cmpFn, colloid, eranges, neranges);
+
+	/*
+	 * We've loaded two different lists of expanded ranges, so some of them
+	 * may be overlapping. So walk through them and merge them.
+	 */
+	neranges = merge_overlapping_ranges(cmpFn, colloid, eranges, neranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidExpandedRanges(bdesc, colloid, attno, attr, eranges, neranges);
+
+	/*
+	 * If needed, reduce some of the ranges.
+	 *
+	 * XXX This may be fairly expensive, so maybe we should do it only when
+	 * it's actually needed (when we have too many ranges).
+	 */
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many of them to get below the threshold. The
+	 * collapsed ranges will be stored as a single value.
+	 *
+	 * XXX This does not apply the load factor, as we don't expect to add more
+	 * values to the range, so we prefer to keep as many ranges as possible.
+	 *
+	 * XXX Can the maxvalues be different in the two ranges? Perhaps we should
+	 * use maximum of those?
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  ranges_a->maxvalues,
+									  cmpFn, colloid);
+
+	/* update the first range summary */
+	store_expanded_ranges(ranges_a, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+								   uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 8, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges	   *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges->maxvalues, ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum		a,
+					b;
+		text	   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum		a;
+		text	   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..69ce6dfca4 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(brdesc,
+												  tuple->bt_columns[keyno].bv_mem_value,
+												  tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -169,15 +177,15 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
 		{
-			Datum value = tuple->bt_columns[keyno].bv_values[datumno];
+			Datum		value = tuple->bt_columns[keyno].bv_values[datumno];
 
 #ifdef TOAST_INDEX_HACK
 
 			/* We must look at the stored type, not at the index descriptor. */
-			TypeCacheEntry	*atttype = brdesc->bd_info[keyno]->oi_typcache[datumno];
+			TypeCacheEntry *atttype = brdesc->bd_info[keyno]->oi_typcache[datumno];
 
 			/* Do we need to free the value at the end? */
-			bool free_value = false;
+			bool		free_value = false;
 
 			/* For non-varlena types we don't need to do anything special */
 			if (atttype->typlen != -1)
@@ -193,9 +201,9 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 * If value is stored EXTERNAL, must fetch it so we are not
 			 * depending on outside storage.
 			 *
-			 * XXX Is this actually true? Could it be that the summary is
-			 * NULL even for range with non-NULL data? E.g. degenerate bloom
-			 * filter may be thrown away, etc.
+			 * XXX Is this actually true? Could it be that the summary is NULL
+			 * even for range with non-NULL data? E.g. degenerate bloom filter
+			 * may be thrown away, etc.
 			 */
 			if (VARATT_IS_EXTERNAL(DatumGetPointer(value)))
 			{
@@ -205,8 +213,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			}
 
 			/*
-			 * If value is above size target, and is of a compressible datatype,
-			 * try to compress it in-line.
+			 * If value is above size target, and is of a compressible
+			 * datatype, try to compress it in-line.
 			 */
 			if (!VARATT_IS_EXTENDED(DatumGetPointer(value)) &&
 				VARSIZE(DatumGetPointer(value)) > TOAST_INDEX_TARGET &&
@@ -495,6 +503,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +587,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 8cc4e532e6..89254b5766 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for minmax-multi indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..87de94f397 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,11 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+/*
+ * The BRIN opclasses may register serialization callback, in case the on-disk
+ * and in-memory representations differ (e.g. for performance reasons).
+ */
+typedef void (*brin_serialize_callback_type) (BrinDesc *bdesc, Datum src, Datum *dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +32,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 82e874130d..654584a03f 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 04d678f96a..8135854163 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2062,6 +2208,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2088,6 +2251,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2155,6 +2334,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2180,6 +2425,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2202,6 +2464,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2224,6 +2503,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2288,6 +2584,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2439,6 +2752,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2470,6 +2929,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2492,6 +2968,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2548,6 +3041,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2570,6 +3080,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2636,6 +3163,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 6709c8dfea..51403716b1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index dea9adaf98..ba9231ac8c 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9926',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '9901',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9927',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '9902',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9928',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '9904',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9929',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '9905',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9930',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '9909',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '9910',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9931',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9932',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '9911',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9933',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '9912',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9934',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '9913',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9935',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '9914',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9936',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '9916',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9937',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '9917',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9938',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '9918',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9939',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '9919',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 36c1433379..8baafdd7c6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8212,6 +8212,77 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9940', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9941', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9942', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9943', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9944', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9945', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9946', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9947', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9948', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9949', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9950', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9951', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9952', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9953', descr => 'BRIN multi minmax date distance',
+  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
+{ oid => '9954', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9955', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9956', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9957', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9958', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9959', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9960', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+{ oid => '9961', descr => 'BRIN multi minmax timestamp distance',
+  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11436,4 +11507,18 @@
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
 
+{ oid => '9962', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9963', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9964', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9965', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 74e279cbf9..e809094490 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -685,4 +685,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+{ oid => '9966',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..e13cb59c7e
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,445 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+ERROR:  value 7 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+DROP INDEX brinidx_multi;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index e568b9fea2..0541c12a25 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9925 | pg_brin_bloom_summary
-(5 rows)
+ 9966 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ecd0806718..d34fc7ef88 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index c0d7fa76f1..91834a984e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..6d61fb84c6
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,397 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+
+DROP INDEX brinidx_multi;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0008-Ignore-correlation-for-new-BRIN-opclasses-20210308b.patchtext/x-patch; charset=UTF-8; name=0008-Ignore-correlation-for-new-BRIN-opclasses-20210308b.patchDownload
From 49bae073136304d01caec4a8239a37b3af35f2cc Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 8/8] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index c086e83236..22590b2351 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -412,6 +412,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index d2dc38adc8..6b1dd1040c 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1737,6 +1737,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 52314d3aa1..0320d128f6 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 89254b5766..063b703208 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

#158John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#156)
Re: WIP: BRIN multi-range indexes

On Sun, Mar 7, 2021 at 8:53 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:
[v20210308b]

I managed to trap an assertion that somehow doesn't happen during the
regression tests. The callers of fill_expanded_ranges() do math like this:

/* both ranges and points are expanded into a separate element */
neranges = ranges->nranges + ranges->nvalues;

but inside fill_expanded_ranges() we have this assertion:

/* Check that the output array has the right size. */
Assert(neranges == (2 * ranges->nranges + ranges->nvalues));

In the regression test data, a debugging elog() shows that nranges is most
often zero, so in that case, the math happens to be right either way. I can
reliably get nranges above zero by running

update brintest_multi set int8col = int8col - 1;

a few times, at which point I get the crash.

Aside from that, the new changes look good. Just a couple small things:

+    allowed parameters.  Only the <literal>bloom</literal> operator class
+    allows specifying parameters:

minmax-multi aren't mentioned here, but are mentioned further down.

+ * Addresses from different families are consider to be in maximum

(comment above brin_minmax_multi_distance_inet)
s/consider/considered/

2) moving minmax/inclusion changes from 0002 to a separate patch 0003

I think we should either ditch the 0003 (i.e. keep the existing
opclasses unchanged) or commit 0003 (in which case I'd vote to just stop
supporting the old signature of the consistent function).

Still not sure what do to about this. I'm leaning towards keeping 0003
and just removing the "old" signature entirely, to keep the API cleaner.
It might cause some breakage in out-of-core BRIN opclasses, but that
seems like a reasonable price. Moreover, the opclasses may need some
updating anyway, because of the changes in handling NULL scan keys (0004
moves that from the opclass to the bringetbitmap function).

Keeping 0003 seems reasonable, given the above.

The remaining part that didn't get much review is the very last patch,
adding an option to ignore correlation for some BRIN opclases. This is
needed as the regular BRIN costing is quite sensitive to correlation,
and the cost gets way too high for poorly correlated data, making it
unlikely the index will be used. But handling such data sets efficiently
is the main point of those new opclasses. Any opinions on this?

Not sure about this.

I hadn't given it much thought (nor tested), but I just took a look at
brincostestimate(). If the table is badly correlated, I'm thinking the
number of ranges that need to be scanned will increase regardless. Maybe
rather than ignoring correlation, we could clamp it or otherwise tweak it.
Not sure about the details, though, that would require some testing.

--
John Naylor
EDB: http://www.enterprisedb.com

#159Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#158)
Re: WIP: BRIN multi-range indexes

On 3/9/21 9:51 PM, John Naylor wrote:

On Sun, Mar 7, 2021 at 8:53 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:
[v20210308b]

I managed to trap an assertion that somehow doesn't happen during the
regression tests. The callers of fill_expanded_ranges() do math like this:

/* both ranges and points are expanded into a separate element */
neranges = ranges->nranges + ranges->nvalues;

but inside fill_expanded_ranges() we have this assertion:

/* Check that the output array has the right size. */
Assert(neranges == (2 * ranges->nranges + ranges->nvalues));

Yeah, that assert is bogus. It's calculating the number of boundary
values (and ranges have two), but in ExpandedRanges each we still
represent that as a single element. So the Assert should be just

Assert(neranges == (ranges->nranges + ranges->nvalues));

But maybe it's just an overkill and we don't really need it.

In the regression test data, a debugging elog() shows that nranges is
most often zero, so in that case, the math happens to be right either
way. I can reliably get nranges above zero by running

update brintest_multi set int8col = int8col - 1;

a few times, at which point I get the crash.

Hmm, so maybe we should do something like this in regression tests too?
It's not good that we don't trigger the "nranges > 0" case at all.

Aside from that, the new changes look good. Just a couple small things:

+    allowed parameters.  Only the <literal>bloom</literal> operator class
+    allows specifying parameters:

minmax-multi aren't mentioned here, but are mentioned further down.

I forgot to add this bit. Will fix.

+ * Addresses from different families are consider to be in maximum

(comment above brin_minmax_multi_distance_inet)
s/consider/considered/

Will fix.

2) moving minmax/inclusion changes from 0002 to a separate patch 0003

I think we should either ditch the 0003 (i.e. keep the existing
opclasses unchanged) or commit 0003 (in which case I'd vote to just stop
supporting the old signature of the consistent function).

Still not sure what do to about this. I'm leaning towards keeping 0003
and just removing the "old" signature entirely, to keep the API cleaner.
It might cause some breakage in out-of-core BRIN opclasses, but that
seems like a reasonable price. Moreover, the opclasses may need some
updating anyway, because of the changes in handling NULL scan keys (0004
moves that from the opclass to the bringetbitmap function).

Keeping 0003 seems reasonable, given the above.

And do you agree with removing the old signature entirely? That might
break some out-of-core opclasses, but we're not aware of any, and
they'll be broken anyway. Seems fine to me.

The remaining part that didn't get much review is the very last patch,
adding an option to ignore correlation for some BRIN opclases. This is
needed as the regular BRIN costing is quite sensitive to correlation,
and the cost gets way too high for poorly correlated data, making it
unlikely the index will be used. But handling such data sets efficiently
is the main point of those new opclasses. Any opinions on this?

Not sure about this.

I hadn't given it much thought (nor tested), but I just took a look at
brincostestimate(). If the table is badly correlated, I'm thinking the
number of ranges that need to be scanned will increase regardless. Maybe
rather than ignoring correlation, we could clamp it or otherwise tweak
it. Not sure about the details, though, that would require some testing.

Well, maybe. In any case we need to do something about this, otherwise
the new opclasses won't be used even in cases where it's perfectly OK.
And it needs to be opclass-dependent, in some way.

I'm pretty sure even the simple examples you've used to test
minmax-multi (with updating a fraction of tuples to low/high value)
would be affected by this.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#160Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#159)
10 attachment(s)
Re: WIP: BRIN multi-range indexes

Hi,

Here is an updated version of the patch series.

It fixes the assert failure (just remove the multiplication from it) and
adds a simple regression test that exercises this.

Based on the discussion so far, I've decided to keep just the new
signature of the consistent function. That's a bit simpler than having
to support both 3 and 4 parameters, and it would not deal with the NULL
changes anyway (mostly harmless code duplication, but still). I've also
realized the API documentation in SGML needs updating.

At this point, I think 0001-0006 parts are mostly committable.

As for the remaining two parts, the one dealing with correlation may not
be strictly necessary, but not having it (or something like it) may
result in not picking the BRIN index in some cases.

But maybe it's not a major problem. I tried the example from [1]/messages/by-id/20200807162701.v7lyzun32lqku3md@development but it
no longer triggers the issue for me - I'm not entirely sure why, but the
costing changed for some reason. It used to look like this:

QUERY PLAN
------------------------------------------------------------------------
Bitmap Heap Scan on t (cost=5553.07..174800.78 rows=10 width=4)
(actual time=7.790..27.585 rows=10 loops=1)
Recheck Cond: (a = 10000)
Rows Removed by Index Recheck: 224182
Heap Blocks: lossy=992
-> Bitmap Index Scan on t_a_idx
(cost=0.00..5553.06 rows=9999977 width=0)
(actual time=7.006..7.007 rows=9920 loops=1)
Index Cond: (a = 10000)
Planning Time: 0.052 ms
Execution Time: 27.658 ms
(8 rows)

but now I'm getting this:

QUERY PLAN
------------------------------------------------------------------------
Bitmap Heap Scan on t (cost=5546.22..97469.16 rows=10 width=4)
(actual time=5.264..32.051 rows=10 loops=1)
Recheck Cond: (a = 10000)
Rows Removed by Index Recheck: 314582
Heap Blocks: lossy=1392
-> Bitmap Index Scan on t_a_idx
(cost=0.00..5546.22 rows=3813995 width=0)
(actual time=2.534..2.536 rows=13920 loops=1)
Index Cond: (a = 10000)
Planning Time: 0.052 ms
Execution Time: 32.095 ms
(8 rows)

The index scan cost is about the same, but the heap scan is about half
the cost. The row estimates are a bit different, perhaps that's related.
The seqscan cost (169248) and duration (~500ms) is still about the same,
but so now we still pick the bitmap heap scan. Not sure we can rely on
this, though. Would be quite sad if we had new improved opclasses but
the planner often decided not to use them.

While playing with this, I discovered another weirdness - consider this
example (a more complete reproducer.sql script attached). If you try it
without the 0008 part, you'll get this:

init
----

create table t (a int, b bigint, c text) with (fillfactor = 50);

insert into t select
1000000 * random(),
1000000 * random(),
md5((1000000 * random())::int::text)
from generate_series(1,1000000) s(i);

create index bloom_a_idx on t using brin
(a int4_bloom_ops(n_distinct_per_range=6000));

analyze t;

query
-----

explain select * from t where a = 1000;

QUERY PLAN
-----------------------------------------------------------------
Bitmap Heap Scan on t (cost=660.03..14000.00 rows=2 width=45)
Recheck Cond: (a = 1000)
-> Bitmap Index Scan on bloom_a_idx
(cost=0.00..660.03 rows=6135 width=0)
Index Cond: (a = 1000)
(4 rows)

So far so good - we're using the right index.

create another index (regular minmax on random data)
----------------------------------------------------

create index brin_idx on t using brin (a);

analyze t;

query (again)
-------------

explain select * from t where a = 1000;

QUERY PLAN
------------------------------------------------------
Seq Scan on t (cost=0.00..33334.00 rows=2 width=45)
Filter: (a = 1000)
(2 rows)

Umm, what? We've created an index, we don't pick it, but it somehow
blocks us from using the right index? So we end up with a sequential
scan, which is much more expensive (both in terms of cost and duration)
than using the bloom index.

Turns out this is due to how choose_bitmap_and eliminates index paths
with the same set of clauses - it simply compares the cost for reading
the index itself. And the bloom filter is larger than plain minmax,
hence it loses. But this entirely ignores the cost increase at the heap
scan level - the minmax index is absulutely terrible, and it loses not
just to bloom index (which we've discarded already) but also to seqscan.

I had an idea of tweaking choose_bitmap_and to consider both the cost
and selectivity (similarly to how add_path considers statup/total cost),
and that did indeed resolve this particular case. This is what the 0008
part does.

But it may also have negative consequence, as demonstrated by the
reproducer2.sql script. So maybe the logic would need to be more
complicated. Or maybe there's no reliable solution, considering how
tricky/unreliable BRIN estimates are.

That being said, I don't think this is something we need to solve here,
and it may not actually be an issue at all. For this to happen there
need to be a terrible index on the same attribute (like the minmax index
in the example above). But why keeping such index anyway? Dropping it
would make the issue go away. If we have two indexes that both perform
reasonably (say, bloom and minmax-multi), the consequences are not that
bad. so this is interesting, but probably fine.

[1]: /messages/by-id/20200807162701.v7lyzun32lqku3md@development
/messages/by-id/20200807162701.v7lyzun32lqku3md@development

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-introduce-bsearch_arg-20210311.patchtext/x-patch; charset=UTF-8; name=0001-introduce-bsearch_arg-20210311.patchDownload
From f3a647439ca3a35ef98da8b8769202ed275dffab Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Fri, 5 Mar 2021 00:16:40 +0100
Subject: [PATCH 1/8] introduce bsearch_arg

---
 src/backend/statistics/extended_stats.c       | 31 --------------
 src/include/port.h                            |  5 +++
 .../statistics/extended_stats_internal.h      |  5 ---
 src/port/Makefile                             |  1 +
 src/port/bsearch_arg.c                        | 40 +++++++++++++++++++
 src/tools/msvc/Mkvcbuild.pm                   |  2 +-
 6 files changed, 47 insertions(+), 37 deletions(-)
 create mode 100644 src/port/bsearch_arg.c

diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index a030ea3653..fa42851fd5 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -659,37 +659,6 @@ compare_datums_simple(Datum a, Datum b, SortSupport ssup)
 	return ApplySortComparator(a, false, b, false, ssup);
 }
 
-/* simple counterpart to qsort_arg */
-void *
-bsearch_arg(const void *key, const void *base, size_t nmemb, size_t size,
-			int (*compar) (const void *, const void *, void *),
-			void *arg)
-{
-	size_t		l,
-				u,
-				idx;
-	const void *p;
-	int			comparison;
-
-	l = 0;
-	u = nmemb;
-	while (l < u)
-	{
-		idx = (l + u) / 2;
-		p = (void *) (((const char *) base) + (idx * size));
-		comparison = (*compar) (key, p, arg);
-
-		if (comparison < 0)
-			u = idx;
-		else if (comparison > 0)
-			l = idx + 1;
-		else
-			return (void *) p;
-	}
-
-	return NULL;
-}
-
 /*
  * build_attnums_array
  *		Transforms a bitmap into an array of AttrNumber values.
diff --git a/src/include/port.h b/src/include/port.h
index 227ef4b148..82f63de325 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -508,6 +508,11 @@ typedef int (*qsort_arg_comparator) (const void *a, const void *b, void *arg);
 extern void qsort_arg(void *base, size_t nel, size_t elsize,
 					  qsort_arg_comparator cmp, void *arg);
 
+extern void *bsearch_arg(const void *key, const void *base,
+						 size_t nmemb, size_t size,
+						 int (*compar) (const void *, const void *, void *),
+						 void *arg);
+
 /* port/chklocale.c */
 extern int	pg_get_encoding_from_locale(const char *ctype, bool write_message);
 
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index c849bd57c0..a0a3cf5b0f 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -85,11 +85,6 @@ extern int	multi_sort_compare_dims(int start, int end, const SortItem *a,
 extern int	compare_scalars_simple(const void *a, const void *b, void *arg);
 extern int	compare_datums_simple(Datum a, Datum b, SortSupport ssup);
 
-extern void *bsearch_arg(const void *key, const void *base,
-						 size_t nmemb, size_t size,
-						 int (*compar) (const void *, const void *, void *),
-						 void *arg);
-
 extern AttrNumber *build_attnums_array(Bitmapset *attrs, int *numattrs);
 
 extern SortItem *build_sorted_items(int numrows, int *nitems, HeapTuple *rows,
diff --git a/src/port/Makefile b/src/port/Makefile
index e41b005c4f..52dbf5783f 100644
--- a/src/port/Makefile
+++ b/src/port/Makefile
@@ -40,6 +40,7 @@ LIBS += $(PTHREAD_LIBS)
 OBJS = \
 	$(LIBOBJS) \
 	$(PG_CRC32C_OBJS) \
+	bsearch_arg.o \
 	chklocale.o \
 	erand48.o \
 	inet_net_ntop.o \
diff --git a/src/port/bsearch_arg.c b/src/port/bsearch_arg.c
new file mode 100644
index 0000000000..d24dc4b7c4
--- /dev/null
+++ b/src/port/bsearch_arg.c
@@ -0,0 +1,40 @@
+/*
+ *	bsearch_arg.c: bsearch variant with a user-supplied pointer
+ *
+ *	src/port/bsearch_arg.c
+ */
+
+
+#include "c.h"
+
+
+/* simple counterpart to qsort_arg */
+void *
+bsearch_arg(const void *key, const void *base, size_t nmemb, size_t size,
+			int (*compar) (const void *, const void *, void *),
+			void *arg)
+{
+	size_t		l,
+				u,
+				idx;
+	const void *p;
+	int			comparison;
+
+	l = 0;
+	u = nmemb;
+	while (l < u)
+	{
+		idx = (l + u) / 2;
+		p = (void *) (((const char *) base) + (idx * size));
+		comparison = (*compar) (key, p, arg);
+
+		if (comparison < 0)
+			u = idx;
+		else if (comparison > 0)
+			l = idx + 1;
+		else
+			return (void *) p;
+	}
+
+	return NULL;
+}
diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm
index 49614106dc..20221c1ae3 100644
--- a/src/tools/msvc/Mkvcbuild.pm
+++ b/src/tools/msvc/Mkvcbuild.pm
@@ -101,7 +101,7 @@ sub mkvcbuild
 	  dirent.c dlopen.c getopt.c getopt_long.c link.c
 	  pread.c preadv.c pwrite.c pwritev.c pg_bitutils.c
 	  pg_strong_random.c pgcheckdir.c pgmkdirp.c pgsleep.c pgstrcasecmp.c
-	  pqsignal.c mkdtemp.c qsort.c qsort_arg.c quotes.c system.c
+	  pqsignal.c mkdtemp.c qsort.c qsort_arg.c bsearch_arg.c quotes.c system.c
 	  strerror.c tar.c thread.c
 	  win32env.c win32error.c win32security.c win32setlocale.c win32stat.c);
 
-- 
2.26.2

0002-Pass-all-scan-keys-to-BRIN-consistent-funct-20210311.patchtext/x-patch; charset=UTF-8; name=0002-Pass-all-scan-keys-to-BRIN-consistent-funct-20210311.patchDownload
From e3f79e8af7f6975e2001f1dfb54b8478f3e79c49 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:04 +0200
Subject: [PATCH 2/8] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow future opclases to benefit from seeing all keys at once.

This does break existing BRIN opclasses, because the signature of the
consistent function changed. We might support both variants (with 3 and
4 parameters), but that seems pointless - the changes to NULL handling
likely require updates too. So better to make the failure clear. Also,
we're not aware of any out-of-core opclasses - there may be some, but
probably not many.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 doc/src/sgml/brin.sgml                   |   8 +-
 src/backend/access/brin/brin.c           | 118 +++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 140 ++++++++++++++++-------
 src/backend/access/brin/brin_minmax.c    |  92 ++++++++++++---
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 6 files changed, 269 insertions(+), 97 deletions(-)

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 06880c0f7b..078f51bb55 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -464,12 +464,14 @@ typedef struct BrinOpcInfo
 
    <varlistentry>
     <term><function>bool consistent(BrinDesc *bdesc, BrinValues *column,
-       ScanKey key)</function></term>
+       ScanKey *keys, int nkeys)</function></term>
     <listitem>
      <para>
-      Returns whether the ScanKey is consistent with the given indexed
-      values for a range.
+      Returns whether all the ScanKey entries are consistent with the given
+      indexed values for a range.
       The attribute number to use is passed as part of the scan key.
+      Multiple scan keys for the same attribute may be passed at once, the
+      number of entries is determined by the <literal>nkeys</literal> parameter.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 27ba596c6e..743f2a925a 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -390,6 +390,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey   **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -411,6 +414,66 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure. We allocate space for all attributes, so
+	 * that we don't have to bother determining which attributes are used.
+	 *
+	 * XXX The widest table can have ~1600 attributes, so this may allocate a
+	 * couple kilobytes of memory). We could invent a more compact approach
+	 * (with just space for used attributes) but that would make the matching
+	 * more complicated, so it may not be a win.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation used in the
+		 * index column (but only if the search is not IS NULL/ IS NOT NULL).
+		 * Otherwise we shouldn't be using this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno - 1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many scan keys
+			 * are there for this attribute, so we simply allocate the largest
+			 * number possible. This may waste a bit of memory, but we only
+			 * expect small number of scan keys in general, so this should be
+			 * negligible, and it's cheaper than having to repalloc
+			 * repeatedly.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -471,7 +534,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int			attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -481,51 +544,40 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
+					Oid			collation;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
-
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
-
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					/* skip attributes without any scan keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
+
+					bval = &dtup->bt_columns[attno - 1];
+
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * XXX We simply use the collation from the first key (it
+					 * has to be the same for all keys for the same attribue).
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
+					collation = keys[attno - 1][0]->sk_collation;
+
+					/* Check all keys at once */
+					add = FunctionCall4Coll(&consistentFn[attno - 1],
+											collation,
 											PointerGetDatum(bdesc),
 											PointerGetDatum(bval),
-											PointerGetDatum(key));
+											PointerGetDatum(keys[attno - 1]),
+											Int32GetDatum(nkeys[attno - 1]));
 					addrange = DatumGetBool(add);
+
 					if (!addrange)
 						break;
 				}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..a260074c91 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -251,6 +253,10 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 /*
  * BRIN inclusion consistent function
  *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches all those keys, we check the regular scan keys.
+ *
  * All of the strategies are optional.
  */
 Datum
@@ -258,24 +264,31 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/* Handle IS NULL/IS NOT NULL tests */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -284,7 +297,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -293,7 +311,14 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
@@ -301,10 +326,45 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all regular scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
+	/* This should be called only for regular keys, not for IS [NOT] NULL. */
+	Assert(!(key->sk_flags & SK_ISNULL));
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +384,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +444,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..e116084a02 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -140,29 +142,41 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's min/max
  * values.  Return true if so, false otherwise.
+ *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches all those keys, we check the regular scan keys.
  */
 Datum
 brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
 	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -171,7 +185,12 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -180,13 +199,52 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
+	}
+
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
+
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +287,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..2c4f9a3eff 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											4, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c7619f8cd3..cd1eda38ab 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8207,7 +8207,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8223,7 +8223,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.26.2

0003-Move-IS-NOT-NULL-handling-from-BRIN-support-20210311.patchtext/x-patch; charset=UTF-8; name=0003-Move-IS-NOT-NULL-handling-from-BRIN-support-20210311.patchDownload
From 3b3ee67673223d7c5574d2c68337720f6829acb7 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 2 Mar 2021 19:27:48 +0100
Subject: [PATCH 3/8] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Reviewed-by: John Naylor <john.naylor@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c |  96 +-------
 src/backend/access/brin/brin_minmax.c    |  93 +------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 244 insertions(+), 241 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 743f2a925a..ce0f525c21 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+								BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -179,7 +182,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -243,31 +245,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -390,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey   **keys;
-	int		   *nkeys;
+	ScanKey   **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -419,13 +399,18 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * consistent support procedure. We allocate space for all attributes, so
 	 * that we don't have to bother determining which attributes are used.
 	 *
+	 * We keep null and regular keys separate, so that we can pass just the
+	 * regular keys to the consistent function easily.
+	 *
 	 * XXX The widest table can have ~1600 attributes, so this may allocate a
 	 * couple kilobytes of memory). We could invent a more compact approach
 	 * (with just space for used attributes) but that would make the matching
 	 * more complicated, so it may not be a win.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -445,23 +430,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno - 1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan keys are
+		 * there for a given attribute, so we simply allocate the largest
+		 * number possible (as if all scan keys belonged to the same
+		 * attribute). This may waste a bit of memory, but we only expect
+		 * small number of scan keys in general, so this should be negligible,
+		 * and it's probably cheaper than having to repalloc repeatedly.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many scan keys
-			 * are there for this attribute, so we simply allocate the largest
-			 * number possible. This may waste a bit of memory, but we only
-			 * expect small number of scan keys in general, so this should be
-			 * negligible, and it's cheaper than having to repalloc
-			 * repeatedly.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -469,9 +454,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -550,15 +549,58 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					Datum		add;
 					Oid			collation;
 
-					/* skip attributes without any scan keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular and
+					 * IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine
+					 * mismatch within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all
+					 * passed. If there are no regular scan keys, we're done -
+					 * the page range matches. If there are regular keys, but
+					 * the page range is marked as 'all nulls' it can't
+					 * possibly pass (we're assuming the operators are
+					 * strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -665,7 +707,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -694,25 +735,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1491,6 +1515,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1544,3 +1601,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values; our
+	 * deformed tuple will get updated if the new tuple doesn't fit the
+	 * original range (note this means we can't break out of the loop early).
+	 * Make a note of whether this happens, so that we know to insert the
+	 * modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's the
+			 * first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index a260074c91..b17077703c 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -268,52 +258,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* Handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
 
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
-
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -331,9 +278,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -574,37 +520,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index e116084a02..330bed0487 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -156,52 +146,9 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
 
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -215,9 +162,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -307,34 +253,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 78c89a6961..79440ebe7b 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0004-Optimize-allocations-in-bringetbitmap-20210311.patchtext/x-patch; charset=UTF-8; name=0004-Optimize-allocations-in-bringetbitmap-20210311.patchDownload
From a62faf9b98d454dee7058dcff150b01fa9f076e9 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 2 Mar 2021 19:57:27 +0100
Subject: [PATCH 4/8] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 60 ++++++++++++++++++++++++++--------
 1 file changed, 47 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index ce0f525c21..758c47f0f4 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -402,15 +405,52 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * We keep null and regular keys separate, so that we can pass just the
 	 * regular keys to the consistent function easily.
 	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly the
+	 * same lifetime, so that's OK.
+	 *
 	 * XXX The widest table can have ~1600 attributes, so this may allocate a
 	 * couple kilobytes of memory). We could invent a more compact approach
 	 * (with just space for used attributes) but that would make the matching
 	 * more complicated, so it may not be a win.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* regular keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* NULL keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -444,9 +484,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -457,17 +497,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.26.2

0005-BRIN-bloom-indexes-20210311.patchtext/x-patch; charset=UTF-8; name=0005-BRIN-bloom-indexes-20210311.patchDownload
From 40838ce08cd87ea5632c79d8b829862c09adf489 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Wed, 16 Dec 2020 21:24:41 +0100
Subject: [PATCH 5/8] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired
false positive rate.

The opclasses store 32-bit hashes of the values, not the raw indexed
values. This assumes the hash functions for data types has low number
of collisions, good performance etc. Collisions are not a huge issue
though, because the number of values in a BRIN ranges is fairly small.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 230 ++++++-
 doc/src/sgml/ref/create_index.sgml        |   1 +
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 787 ++++++++++++++++++++++
 src/include/access/brin.h                 |   2 +
 src/include/access/brin_internal.h        |   4 +
 src/include/catalog/pg_amop.dat           | 116 ++++
 src/include/catalog/pg_amproc.dat         | 447 ++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 ++
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +-
 src/test/regress/expected/brin_bloom.out  | 428 ++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 376 +++++++++++
 19 files changed, 2555 insertions(+), 7 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 078f51bb55..b3fc7166e2 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -115,7 +115,8 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
   operator classes store the minimum and the maximum values appearing
   in the indexed column within the range.  The <firstterm>inclusion</firstterm>
   operator classes store a value which includes the values in the indexed
-  column within the range.
+  column within the range.  The <firstterm>bloom</firstterm> operator
+  classes build a Bloom filter for all values in the range.
  </para>
 
  <table id="brin-builtin-opclasses-table">
@@ -128,6 +129,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     </row>
    </thead>
    <tbody>
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bit,bit)</literal></entry>
+    </row>
     <row>
      <entry valign="middle" morerows="4"><literal>bit_minmax_ops</literal></entry>
      <entry><literal>= (bit,bit)</literal></entry>
@@ -154,6 +159,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +173,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +187,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +201,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +215,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +229,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +253,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +267,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +281,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +295,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +309,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +323,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +337,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +351,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +365,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +379,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +393,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +425,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +439,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +453,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +467,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +481,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +495,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +509,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -409,6 +534,55 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    </tbody>
   </tgroup>
  </table>
+
+  <sect2 id="brin-builtin-opclasses--parameters">
+   <title>Operator Class Parameters</title>
+
+   <para>
+    Some of the built-in operator classes allow specifying parameters affecting
+    behavior of the operator class.  Each operator class has its own set of
+    allowed parameters.  Only the <literal>bloom</literal> operator class
+    allows specifying parameters:
+   </para>
+
+   <para>
+    <acronym>bloom</acronym> operator classes accept these parameters:
+   </para>
+
+   <variablelist>
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default value is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be
+     between 0.0001 and 0.25. The default value is 0.01, which is 1% false
+     positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   </variablelist>
+  </sect2>
+
 </sect1>
 
 <sect1 id="brin-extensibility">
@@ -781,6 +955,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 51b4d57939..4a29935785 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -581,6 +581,7 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
     </para>
     </listitem>
    </varlistentry>
+
    </variablelist>
   </refsect2>
 
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..4494484b3b
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,787 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a
+ * bloom filter, we can easily and cheaply test whether it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * BRIN bloom indexes are however much smaller, and support only bitmap
+ * scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random, compression can't reliably help here.
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This does help even for random data sets, as
+ * the number of rows per heap page is limited (to ~290 with very narrow
+ * tables, likely ~20 in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distrubute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_page.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001	/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25	/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+
+#define BloomMaxFilterSize \
+	MAXALIGN_DOWN(BLCKSZ - \
+				  (MAXALIGN(SizeOfPageHeaderData + \
+							sizeof(ItemIdData)) + \
+				   MAXALIGN(sizeof(BrinSpecialSpace)) + \
+				   SizeOfBrinTuple))
+
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * To calculate the additional hashes (treating the uint32 hash as an input
+ * value), we use the approach with two hash functions from this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions are calculated using hard-coded seeds.
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes
+ * that are not entirely 0. But while indexes don't support TOAST, the
+ * varlena can still be compressed. So this seems unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high. But even if the false positive rate
+ * exceeds the desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16		flags;
+
+	/* fields for the HASHED phase */
+	uint8		nhashes;		/* number of hash functions */
+	uint32		nbits;			/* number of bits in the bitmap (size) */
+	uint32		nbits_set;		/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+
+}			BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size		len;
+	BloomFilter *filter;
+
+	int			nbits;			/* size of filter / number of bits */
+	int			nbytes;			/* size of filter / number of bytes */
+
+	double		k;				/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	nbits = ceil(-(ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	nbytes = ((nbits + 7) / 8);
+	nbits = nbytes * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * Initially the bloom filter is just zeroes and so very compressible, but
+	 * as we add values it gets more and more random, and so less and less
+	 * compressible. So initially everything fits on the page, but we might
+	 * get surprising failures later - we want to prevent that, so we reject
+	 * bloom filter that are obviously too large.
+	 *
+	 * XXX It's not uncommon to oversize the bloom filter a bit, to defend
+	 * against unexpected data anomalies (parts of table with more distinct
+	 * values per range etc.). But we still need to make sure even the
+	 * oversized filter fits on page, if such need arises.
+	 *
+	 * XXX This check is not perfect, because the index may have multiple
+	 * filters that are small individually, but too large when combined.
+	 */
+	if (nbytes > BloomMaxFilterSize)
+		elog(ERROR, "the bloom filter is too large (%d > %zu)", nbytes,
+			 BloomMaxFilterSize);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * nbits / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so the
+	 * varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + nbytes;
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = nbits;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter * filter, uint32 value, bool *updated)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (!(filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter * filter, uint32 value)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* calculate the two hashes */
+	h1 = DatumGetUInt64(hash_uint32_extended(value, 0x71d924af)) % filter->nbits;
+	h2 = DatumGetUInt64(hash_uint32_extended(value, 0xba48b314)) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (!(filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash), but
+	 * let's keep the array just like inclusion and minman opclasses, for
+	 * consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+}			BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+									uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double		ndistinct;
+	double		maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this case to
+	 * maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a couple of
+	 * safeties no to use unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples, in the
+	 * range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from
+	 * BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to
+				 * return the current page range if the minimum value in the
+				 * range <= scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters for now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing text
+	 * input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 4e2be13cd6..0e52d75457 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -22,6 +22,8 @@ typedef struct BrinOptions
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	BlockNumber pagesPerRange;
 	bool		autosummarize;
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 79440ebe7b..8cc4e532e6 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -58,6 +58,10 @@ typedef struct BrinDesc
 	/* total number of Datum entries that are stored on-disk for all columns */
 	int			bd_totalstored;
 
+	/* parameters for sizing bloom filter (BRIN bloom opclasses) */
+	double		bd_nDistinctPerRange;
+	double		bd_falsePositiveRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..04d678f96a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,20 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2040,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2062,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2084,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2155,14 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2180,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2202,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2224,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2266,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2288,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2439,20 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2470,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2492,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2548,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2570,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2636,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..6709c8dfea 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..dea9adaf98 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9901',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '9902',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '9903',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9904',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9905',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '9906',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '9907',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '9908',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9909',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '9910',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9911',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9912',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9913',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '9914',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '9915',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9916',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9917',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9918',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9919',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cd1eda38ab..505120f963 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8230,6 +8230,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '9920', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '9921', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '9922', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '9923', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '9924', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11403,4 +11423,18 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
+{ oid => '9035', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '9036', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '9037', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '9038', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8959c2f53b..74e279cbf9 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,5 +679,10 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-
+{ oid => '9925',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..32c56a996a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,428 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..e568b9fea2 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
-(4 rows)
+ 9925 | pg_brin_bloom_summary
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index e280198b17..3dbea5b2cc 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 6a57e889a1..e5c873e0b4 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..5d499208e3
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,376 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.26.2

0006-BRIN-minmax-multi-indexes-20210311.patchtext/x-patch; charset=UTF-8; name=0006-BRIN-minmax-multi-indexes-20210311.patchDownload
From 57354d647a859bc7966586ee3e58b285e1d723d3 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Wed, 3 Feb 2021 19:00:00 +0100
Subject: [PATCH 6/8] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Reviewed-by: Alvaro Herrera <alvherre@2ndquadrant.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  280 +-
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_bloom.c        |    4 +-
 src/backend/access/brin/brin_minmax_multi.c | 2982 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   33 +-
 src/include/access/brin.h                   |    1 +
 src/include/access/brin_internal.h          |    3 +
 src/include/access/brin_tuple.h             |    8 +
 src/include/access/transam.h                |    2 +-
 src/include/catalog/pg_amop.dat             |  544 ++++
 src/include/catalog/pg_amproc.dat           |  600 +++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   85 +
 src/include/catalog/pg_type.dat             |    6 +
 src/test/regress/expected/brin_multi.out    |  450 +++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  403 +++
 21 files changed, 5478 insertions(+), 32 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index b3fc7166e2..cba8ecb851 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -116,7 +116,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
   in the indexed column within the range.  The <firstterm>inclusion</firstterm>
   operator classes store a value which includes the values in the indexed
   column within the range.  The <firstterm>bloom</firstterm> operator
-  classes build a Bloom filter for all values in the range.
+  classes build a Bloom filter for all values in the range.  The
+  <firstterm>minmax-multi</firstterm> operator classes store multiple
+  minimum and maximum values, representing values appearing in the indexed
+  column within the range.
  </para>
 
  <table id="brin-builtin-opclasses-table">
@@ -215,6 +218,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -229,6 +241,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -243,6 +264,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -267,6 +297,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -281,6 +320,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -295,6 +343,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -309,6 +366,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -323,6 +389,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -337,6 +412,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -351,6 +435,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -379,6 +472,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -393,6 +495,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -407,6 +518,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -453,6 +573,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -467,6 +596,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -481,6 +619,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -495,6 +642,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -509,6 +665,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -523,6 +688,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -541,8 +715,8 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    <para>
     Some of the built-in operator classes allow specifying parameters affecting
     behavior of the operator class.  Each operator class has its own set of
-    allowed parameters.  Only the <literal>bloom</literal> operator class
-    allows specifying parameters:
+    allowed parameters.  Only the <literal>bloom</literal> and <literal>minmax-multi</literal>
+    operator classes allow specifying parameters:
    </para>
 
    <para>
@@ -581,6 +755,25 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    </varlistentry>
 
    </variablelist>
+
+   <para>
+    <acronym>minmax-multi</acronym> operator classes accept these parameters:
+   </para>
+
+   <variablelist>
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     either a point, or a boundary of an interval. Values must be between
+     8 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   </variablelist>
   </sect2>
 
 </sect1>
@@ -706,13 +899,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, minmax-multi, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -1009,6 +1203,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The minmax-multi operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, minmax-multi allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the minmax-multi support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-minmax-multi-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-minmax-multi-table">
+  <title>Procedure and Support Numbers for minmax-multi Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 4494484b3b..c086e83236 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -83,7 +83,7 @@
  * the whole index tuple that has to fit into a page. And for multi-column
  * indexes that may include pieces we have no control over (not necessarily
  * bloom filters, the other columns may use other BRIN opclasses). So it's
- * not entirely clear how to distrubute the space between those columns.
+ * not entirely clear how to distribute the space between those columns.
  *
  * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
  * make some basic sizing decisions, based on the size of BRIN ranges, and
@@ -468,7 +468,7 @@ brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
 
 	/*
 	 * Positive values are to be used directly, but we still apply a couple of
-	 * safeties no to use unreasonably small bloom filters.
+	 * safeties to avoid using unreasonably small bloom filters.
 	 */
 	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
 
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..d19ebf08c2
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2982 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With minmax-multi opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 50% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values */
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Sizing the insert buffer - we use 10x the number of values specified
+ * in the reloption, but we cap it to 8192 not to get too large. When
+ * the buffer gets full, we reduce the number of values by half.
+ */
+#define		MINMAX_BUFFER_FACTOR			10
+#define		MINMAX_BUFFER_MIN				256
+#define		MINMAX_BUFFER_MAX				8192
+#define		MINMAX_BUFFER_LOAD_FACTOR		0.5
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+}			MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange; /* number of values per range */
+}			MinMaxOptions;
+
+#define MINMAX_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxGetValuesPerRange(opts) \
+		((opts) && (((MinMaxOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxOptions *) (opts))->valuesPerRange : \
+		 MINMAX_DEFAULT_VALUES_PER_PAGE)
+
+#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0))
+
+/*
+ * The summary of minmax-multi indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* Cache information that we need quite often. */
+	Oid			typid;
+	Oid			colloid;
+	AttrNumber	attno;
+	FmgrInfo   *cmp;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nsorted;		/* number of sorted values (ranges + points) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/*
+	 * We simply add the values into a large buffer, without any expensive
+	 * steps (sorting, deduplication, ...). The buffer is a multiple of the
+	 * target number of values, so the compaction happen less often,
+	 * amortizing the costs. We keep the actual target and compact to the
+	 * requested number of values at the very end, before serializing to
+	 * on-disk representation.
+	 */
+	/* requested number of values */
+	int			target_maxvalues;
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum		values[FLEXIBLE_ARRAY_MEMBER];
+}			Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, with a simple header
+ * with basic metadata, followed by the boundary values. It has a varlena
+ * header, so can be treated as varlena directly.
+ *
+ * See range_serialize/range_deserialize for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* type of values stored in the data array */
+	Oid			typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+}			SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(int maxvalues, SerializedRanges *range);
+
+
+/*
+ * Used to represent ranges expanded to make merging and combining easier.
+ *
+ * Each expanded range is essentially an interval, represented by min/max
+ * values, along with a flag whether it's a collapsed range (in which case
+ * the min and max values are equal). We have the flag to handle by-ref
+ * data types - we can't simply compare the datums, and this saves some
+ * calls to the type-specific comparator function.
+ */
+typedef struct ExpandedRange
+{
+	Datum		minval;			/* lower boundary */
+	Datum		maxval;			/* upper boundary */
+	bool		collapsed;		/* true if minval==maxval */
+}			ExpandedRange;
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of extended ranges).
+ */
+typedef struct DistanceValue
+{
+	int			index;
+	double		value;
+}			DistanceValue;
+
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+										   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+													uint16 attno, Oid subtype,
+													uint16 strategynum);
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+}			compare_context;
+
+static int	compare_values(const void *a, const void *b, void *arg);
+
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+	int			i;
+	Datum		lt;
+
+	for (i = 0; i < (nvalues - 1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i + 1]);
+		Assert(DatumGetBool(lt));
+	}
+}
+#endif
+
+/*
+ * Comprehensive check of the Ranges structure.
+ */
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+
+	/* some basic sanity checks */
+	Assert(ranges->nranges >= 0);
+	Assert(ranges->nsorted >= 0);
+	Assert(ranges->nvalues >= ranges->nsorted);
+	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
+	Assert(ranges->typid != InvalidOid);
+
+	/*
+	 * First the ranges - there are 2*nranges boundary values, and the values
+	 * have to be strictly ordered (equal values would mean the range is
+	 * collapsed, and should be stored as a point). This also guarantees that
+	 * the ranges do not overlap.
+	 */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2 * ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2 * ranges->nranges],
+					 ranges->nsorted);
+
+	/*
+	 * Check that none of the values are not covered by ranges (both sorted
+	 * and unsorted)
+	 */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		compar;
+		int			start,
+					end;
+		Datum		minvalue,
+					maxvalue;
+
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nranges == 0)
+			break;
+
+		minvalue = ranges->values[0];
+		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+		/* smaller than the smallest value in the first range */
+		if (DatumGetBool(compar))
+			continue;
+
+		/*
+		 * Is the value greater than the maxval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+		/* larger than the largest value in the last range */
+		if (DatumGetBool(compar))
+			continue;
+
+		start = 0;				/* first range */
+		end = ranges->nranges - 1;	/* last range */
+		while (true)
+		{
+			int			midpoint = (start + end) / 2;
+
+			/* this means we ran out of ranges in the last step */
+			if (start > end)
+				break;
+
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2 * midpoint];
+			maxvalue = ranges->values[2 * midpoint + 1];
+
+			/*
+			 * Is the value smaller than the minval? If yes, we'll recurse to
+			 * the left side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+			{
+				end = (midpoint - 1);
+				continue;
+			}
+
+			/*
+			 * Is the value greater than the minval? If yes, we'll recurse to
+			 * the right side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+			{
+				start = (midpoint + 1);
+				continue;
+			}
+
+			/* hey, we found a matching range */
+			Assert(false);
+		}
+	}
+
+	/* and values in the unsorted part must not be in sorted part */
+	for (i = ranges->nsorted; i < ranges->nvalues; i++)
+	{
+		compare_context cxt;
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nsorted == 0)
+			break;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		Assert(bsearch_arg(&value, &ranges->values[2 * ranges->nranges],
+						   ranges->nsorted, sizeof(Datum),
+						   compare_values, (void *) &cxt) == NULL);
+	}
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidExpandedRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						  Form_pg_attribute attr, ExpandedRange *ranges,
+						  int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+	FmgrInfo   *eq;
+	FmgrInfo   *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum		r;
+		Datum		minval = ranges[i].minval;
+		Datum		maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else					/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e. upper <
+	 * lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges - 1; i++)
+	{
+		Datum		r;
+		Datum		maxval = ranges[i].maxval;
+		Datum		minval = ranges[i + 1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size		len;
+	Ranges	   *ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values); /* fixed header */
+	len += maxvalues * sizeof(Datum);	/* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+
+/*
+ * range_deduplicate_values
+ *		Deduplicate the part with values in the simple points.
+ *
+ * This is meant to be a cheaper way of reducing the size of the ranges. It
+ * does not touch the ranges, and only sorts the other values - it does not
+ * call the distance functions, which may be quite expensive, etc.
+ */
+static void
+range_deduplicate_values(Ranges *range)
+{
+	int			i,
+				n;
+	int			start;
+	compare_context cxt;
+
+	/*
+	 * If there are no unsorted values, we're done (this probably can't
+	 * happen, as we're adding values to unsorted part).
+	 */
+	if (range->nsorted == range->nvalues)
+		return;
+
+	/* sort the values */
+	cxt.colloid = range->colloid;
+	cxt.cmpFn = range->cmp;
+
+	/* how many values to sort? */
+	start = 2 * range->nranges;
+
+	qsort_arg(&range->values[start],
+			  range->nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	n = 1;
+	for (i = 1; i < range->nvalues; i++)
+	{
+		/* same as preceding value, so store it */
+		if (compare_values(&range->values[start + i - 1],
+						   &range->values[start + i],
+						   (void *) &cxt) == 0)
+			continue;
+
+		range->values[start + n] = range->values[start + i];
+
+		n++;
+	}
+
+	/* now all the values are sorted */
+	range->nvalues = n;
+	range->nsorted = n;
+
+	AssertCheckRanges(range, range->cmp, range->colloid);
+}
+
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size		len;
+	int			nvalues;
+	SerializedRanges *serialized;
+	Oid			typid;
+	int			typlen;
+	bool		typbyval;
+
+	int			i;
+	char	   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nsorted >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+	Assert(range->target_maxvalues > 0);
+
+	/* at this point the range should be compacted to the target size */
+	Assert(2 * range->nranges + range->nvalues <= range->target_maxvalues);
+
+	Assert(range->target_maxvalues <= range->maxvalues);
+
+	/* range boundaries are always sorted */
+	Assert(range->nvalues >= range->nsorted);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2 * range->nranges + range->nvalues;
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges, data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)			/* varlena */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)		/* cstring */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else						/* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->target_maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation this
+	 * depends on the particular data type).
+	 */
+	ptr = serialized->data;		/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int			tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int			tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + len));
+	}
+
+	/* exact size */
+	Assert(ptr == ((char *) serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(int maxvalues, SerializedRanges *serialized)
+{
+	int			i,
+				nvalues;
+	char	   *ptr;
+	bool		typbyval;
+	int			typlen;
+
+	Ranges	   *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2 * serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+	Assert(serialized->maxvalues <= maxvalues);
+
+	range = minmax_multi_init(maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->nsorted = serialized->nvalues;
+	range->maxvalues = maxvalues;
+	range->target_maxvalues = serialized->maxvalues;
+
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need to copy
+	 * the values and will instead just point the values to the serialized
+	 * varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *) serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+/*
+ * compare_expanded_ranges
+ *	  Compare the expanded ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_expanded_ranges(const void *a, const void *b, void *arg)
+{
+	ExpandedRange *ra = (ExpandedRange *) a;
+	ExpandedRange *rb = (ExpandedRange *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * compare_values
+ *	  Compare the values.
+ */
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum	   *da = (Datum *) a;
+	Datum	   *db = (Datum *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *da, *db);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *db, *da);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Check if the new value matches one of the existing ranges.
+ */
+static bool
+has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges,
+				   Datum newval, AttrNumber attno, Oid typid)
+{
+	Datum		compar;
+
+	Datum		minvalue = ranges->values[0];
+	Datum		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+
+	/* binary search on ranges */
+	int			start,
+				end;
+
+	if (ranges->nranges == 0)
+		return false;
+
+	/*
+	 * Otherwise, need to compare the new value with boundaries of all the
+	 * ranges. First check if it's less than the absolute minimum, which is
+	 * the first value in the array.
+	 */
+	cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												   BTLessStrategyNumber);
+	compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+	/* smaller than the smallest value in the range list */
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * And now compare it to the existing maximum (last value in the data
+	 * array). But only if we haven't already ruled out a possible match in
+	 * the minvalue check.
+	 */
+	cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													  BTGreaterStrategyNumber);
+	compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * So we know it's in the general min/max, the question is whether it
+	 * falls in one of the ranges or gaps. We'll do a binary search on
+	 * individual ranges - for each range we check equality (value falls
+	 * into the range), and then check ranges either above or below the
+	 * current range.
+	 */
+	start = 0;					/* first range */
+	end = (ranges->nranges - 1);	/* last range */
+	while (true)
+	{
+		int			midpoint = (start + end) / 2;
+
+		/* this means we ran out of ranges in the last step */
+		if (start > end)
+			return false;
+
+		/* copy the min/max values from the ranges */
+		minvalue = ranges->values[2 * midpoint];
+		maxvalue = ranges->values[2 * midpoint + 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in this range */
+		if (DatumGetBool(compar))
+		{
+			end = (midpoint - 1);
+			continue;
+		}
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+		/* larger than the largest value in this range */
+		if (DatumGetBool(compar))
+		{
+			start = (midpoint + 1);
+			continue;
+		}
+
+		/* hey, we found a matching range */
+		return true;
+	}
+
+	return false;
+}
+
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX We only inspect the sorted parts, which means we may add duplicate
+ * values. It may produce some false negatives when adding the values, but
+ * only after we already added some values (otherwise there is no unsorted
+ * part). And when querying the index, there should be no unsorted values,
+ * because the values are sorted and deduplicated during serialization.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+					 AttrNumber attno, Form_pg_attribute attr,
+					 Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid))
+		return true;
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													BTEqualStrategyNumber);
+
+	/*
+	 * There is no matching range, so let's inspect the sorted values.
+	 *
+	 * XXX We do a sequential search for small number of values, and binary
+	 * search once we have more than 16 values. This threshold is somewhat
+	 * arbitrary, as it depends on how expensive the comparison function is.
+	 * So maybe we should just do the binary search all the time.
+	 *
+	 * XXX If we use the threshold here, maybe we should do the same thing in
+	 * has_matching_range?
+	 */
+	if (ranges->nsorted >= 16)
+	{
+		compare_context cxt;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		if (bsearch_arg(&newval, &ranges->values[2 * ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) != NULL)
+			return true;
+	}
+	else
+	{
+		for (i = 2 * ranges->nranges; i < 2 * ranges->nranges + ranges->nsorted; i++)
+		{
+			Datum		compar;
+
+			compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+			/* found an exact match */
+			if (DatumGetBool(compar))
+				return true;
+		}
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * Expand ranges from Ranges into ExpandedRange array. This expects the
+ * eranges to be pre-allocated and with the correct size - there needs to be
+ * (nranges + nvalues) elements.
+ *
+ * The order of expanded ranges is arbitrary. We do expand the ranges first,
+ * and this part is sorted. But then we expand the values, and this part may
+ * be unsorted.
+ */
+static void
+fill_expanded_ranges(ExpandedRange *eranges, int neranges, Ranges *ranges)
+{
+	int			idx;
+	int			i;
+
+	/* Check that the output array has the right size. */
+	Assert(neranges == (ranges->nranges + ranges->nvalues));
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * i];
+		eranges[idx].maxval = ranges->values[2 * i + 1];
+		eranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].maxval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	/* Did we produce the expected number of elements? */
+	Assert(idx == neranges);
+
+	return;
+}
+
+/*
+ * Sort and deduplicate expanded ranges.
+ *
+ * The ranges may be deduplicated - we're simply appending values, without
+ * checking for duplicates etc. So maybe the deduplication will reduce the
+ * number of ranges enough, and we won't have to compute the distances etc.
+ *
+ * Returns the number of expanded ranges.
+ *
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that some of the input data is already sorted (all the ranges
+ * and possibly some of the points) and do merge sort.
+ */
+static int
+sort_expanded_ranges(FmgrInfo *cmp, Oid colloid,
+					 ExpandedRange *eranges, int neranges)
+{
+	int			n;
+	int			i;
+	compare_context cxt;
+
+	Assert(neranges > 0);
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(eranges, neranges, sizeof(ExpandedRange),
+			  compare_expanded_ranges, (void *) &cxt);
+
+	/*
+	 * Deduplicate the ranges - simply compare each range to the preceding
+	 * one, and skip the duplicate ones.
+	 */
+	n = 1;
+	for (i = 1; i < neranges; i++)
+	{
+		/* if the current range is equal to the preceding one, do nothing */
+		if (!compare_expanded_ranges(&eranges[i - 1], &eranges[i], (void *) &cxt))
+			continue;
+
+		/* otherwise copy it to n-th place (if not already there) */
+		if (i != n)
+			memcpy(&eranges[n], &eranges[i], sizeof(ExpandedRange));
+
+		n++;
+	}
+
+	Assert((n > 0) && (n <= neranges));
+
+	return n;
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the expanded ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_overlapping_ranges(FmgrInfo *cmp, Oid colloid,
+						 ExpandedRange *eranges, int neranges)
+{
+	int			idx;
+
+	/* Merge ranges (idx) and (idx+1) if they overlap. */
+	idx = 0;
+	while (idx < (neranges - 1))
+	{
+		Datum		r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap if (minval
+		 * < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges are
+		 * ordered, so there are no more overlaps, because all the remaining
+		 * ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they overlap only partially.
+		 * So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].maxval);
+
+		if (DatumGetBool(r))
+			eranges[idx].maxval = eranges[idx + 1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectively of the
+		 * previous state).
+		 */
+		eranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the remaining
+		 * ranges by 1. There are ncranges elements, and we need to move
+		 * elements from (idx+2). That means the number of elements to move is
+		 * [ncranges - (idx+2)].
+		 */
+		memmove(&eranges[idx + 1], &eranges[idx + 2],
+				(neranges - (idx + 2)) * sizeof(ExpandedRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range, as
+		 * it might overlap with additional ranges thanks to the merge).
+		 */
+		neranges--;
+	}
+
+	return neranges;
+}
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ * This is intentionally sorting the distances in descending order, i.e.
+ * the longer gaps will be at the front.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *) a;
+	DistanceValue *db = (DistanceValue *) b;
+
+	if (da->value < db->value)
+		return 1;
+	else if (da->value > db->value)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Given an array of expanded ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (max-min) for pairs
+ * of consecutive ranges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_expanded_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			ndistances;
+	DistanceValue *distances;
+
+	Assert(neranges >= 2);
+
+	ndistances = (neranges - 1);
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * ndistances);
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges so
+	 * that we can sort them once.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		Datum		a1,
+					a2,
+					r;
+
+		a1 = eranges[i].maxval;
+		a2 = eranges[i + 1].minval;
+
+		/* compute length of the gap (between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index of the gap the distance is for */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/*
+	 * Sort the distances in descending order, so that the longest gaps are at
+	 * the front.
+	 */
+	pg_qsort(distances, ndistances, sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds expanded ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).  This expanded
+ * representation makes the processing a bit easier, as it allows handling
+ * ranges and points the same way.
+ *
+ * We sort and deduplicate the expanded ranges - this is necessary, because
+ * the points may be unsorted. And moreover the two parts (ranges and
+ * points) are sorted on their own.
+ */
+static ExpandedRange *
+build_expanded_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					  int *nranges)
+{
+	int			neranges;
+	ExpandedRange *eranges;
+
+	/* both ranges and points are expanded into a separate element */
+	neranges = ranges->nranges + ranges->nvalues;
+
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges */
+	fill_expanded_ranges(eranges, neranges, ranges);
+
+	/* sort and deduplicate the expanded ranges */
+	neranges = sort_expanded_ranges(cmp, colloid, eranges, neranges);
+
+	/* remember how many cranges we built */
+	*nranges = neranges;
+
+	return eranges;
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Counts boundary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(ExpandedRange *cranges, int ncranges)
+{
+	int			i;
+	int			count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+#endif
+
+/*
+ * reduce_expanded_ranges
+ *		reduce the ranges until the number of values is low enough
+ *
+ * Combines ranges until the number of boundary values drops below the
+ * threshold specified by max_values. This happens by merging enough
+ * ranges by distance between them.
+ *
+ * Returns the number of result ranges.
+ *
+ * We simply use the global min/max and then add boundaries for enough
+ * largest gaps. Each gap adds 2 values, so we simply use (target/2-1)
+ * distances. Then we simply sort all the values - each two values are
+ * a boundary of a range (possibly collapsed).
+ *
+ * XXX Some of the ranges may be collapsed (i.e. the min/max values are
+ * equal), but we ignore that for now. We could repeat the process,
+ * adding a couple more gaps recursively.
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ *
+ * XXX Or maybe we could just handle this by using random value as a
+ * tie-break, or by adding random noise to the actual distance.
+ */
+static int
+reduce_expanded_ranges(ExpandedRange *eranges, int neranges,
+					   DistanceValue *distances, int max_values,
+					   FmgrInfo *cmp, Oid colloid)
+{
+	int			i;
+	int			nvalues;
+	Datum	   *values;
+
+	compare_context cxt;
+
+	/* total number of gaps between ranges */
+	int			ndistances = (neranges - 1);
+
+	/* number of gaps to keep */
+	int			keep = (max_values / 2 - 1);
+
+	/*
+	 * Maybe we have sufficiently low number of ranges already?
+	 *
+	 * XXX This should happen before we actually do the expensive stuff like
+	 * sorting, so maybe this should be just an assert.
+	 */
+	if (keep >= ndistances)
+		return neranges;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	/* add the global min/max values, from the first/last range */
+	values[nvalues++] = eranges[0].minval;
+	values[nvalues++] = eranges[neranges - 1].maxval;
+
+	/* add boundary values for enough gaps */
+	for (i = 0; i < keep; i++)
+	{
+		/* index of the gap between (index) and (index+1) ranges */
+		int			index = distances[i].index;
+
+		Assert((index >= 0) && ((index + 1) < neranges));
+
+		/* add max from the preceding range, minval from the next one */
+		values[nvalues++] = eranges[index].maxval;
+		values[nvalues++] = eranges[index + 1].minval;
+
+		Assert(nvalues <= max_values);
+	}
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/*
+	 * Sort the values using the comparator function, and form ranges from the
+	 * sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		eranges[i].minval = values[2 * i];
+		eranges[i].maxval = values[2 * i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		eranges[i].collapsed = (compare_values(&values[2 * i],
+											   &values[2 * i + 1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+/*
+ * Store the boundary values from ExpandedRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_expanded_ranges(Ranges *ranges, ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (!eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->values[idx++] = eranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+
+	/* all the values are sorted */
+	ranges->nsorted = ranges->nvalues;
+
+	Assert(count_values(eranges, neranges) == 2 * ranges->nranges + ranges->nvalues);
+	Assert(2 * ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+}
+
+
+/*
+ * Consider freeing space in the ranges.
+ *
+ * Returns true if the value was actually modified.
+ */
+static bool
+ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *range)
+{
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	/*
+	 * If there is free space in the buffer, we're done without having to
+	 * modify anything.
+	 */
+	if (2 * range->nranges + range->nvalues < range->maxvalues)
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* did we reduce enough free space by just the deduplication? */
+	if (2 * range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR)
+		return true;
+
+	/*
+	 * We need to combine some of the existing ranges, to reduce the number of
+	 * values we have to store.
+	 *
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it (we might have
+	 * to do this repeatedly, even for a single BRIN page range). Otherwise
+	 * we'd have problems e.g. when building new indexes. So we use a memory
+	 * context and make sure we free the memory at the end (so if we call the
+	 * distance function many times, it might be an issue, but meh).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, colloid, range, &neranges);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * Combine ranges until we release at least 50% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR,
+									  cmpFn, colloid);
+
+	/* Make sure we've sufficiently reduced the number of ranges. */
+	Assert(count_values(eranges, neranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR);
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(range, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Did we break the ranges somehow? */
+	AssertCheckRanges(range, cmpFn, colloid);
+
+	return true;
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the minmax-multi range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn;
+	bool		modified = false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Make sure there's enough free space in the buffer. We only trigger this
+	 * when the buffer is full, which means it had to be modified as we size
+	 * it to be larger than what is stored on disk.
+	 *
+	 * XXX This needs to happen before we check if the value is contained in
+	 * the range, because the value might be in the unsorted part, and we
+	 * don't check that in range_contains_value. The deduplication would then
+	 * move it to the sorted part, and we'd add the value too, which violates
+	 * the rule that we never have duplicates with the ranges or sorted
+	 * values.
+	 *
+	 * XXX At the moment this only does the deduplication.
+	 *
+	 * XXX We might also deduplicate and recheck if the value is contained,
+	 * but that seems like an overkill. We'd need to deduplicate anyway, so
+	 * why not do it now.
+	 */
+	modified = ensure_free_space_in_buffer(bdesc, colloid,
+										   attno, attr, ranges);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then do the
+	 * deduplication in a batch, hoping for better efficiency. But that would
+	 * mean we actually modify the range every time, which means having to
+	 * serialize the value, which does palloc, walks the values, copies them,
+	 * etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming the
+	 * comparator function is not very expensive.
+	 *
+	 * This also implies the values array can't contain duplicate values.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return modified;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we do a
+	 * simple insertion sort. We could do something more elaborate, e.g. by
+	 * sorting the values only now and then, but for small counts (e.g. when
+	 * maxvalues is 64) this should be fine.
+	 */
+	ranges->values[2 * ranges->nranges + ranges->nvalues] = newval;
+	ranges->nvalues++;
+
+	/*
+	 * Check we haven't broken the ordering of boundary values (checks both
+	 * parts, but that doesn't hurt).
+	 */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/* Also check the range contains the value we just added. */
+	/* FIXME Assert(ranges, cmpFn, colloid); */
+
+	/* yep, we've modified the range */
+	return true;
+}
+
+/*
+ * Generate range representation of data collected during "batch mode".
+ * This is similar to reduce_expanded_ranges, except that we can't assume
+ * the values are sorted and there may be duplicate values.
+ */
+static void
+compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
+											   BTLessStrategyNumber);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, ranges->attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, ranges->colloid, ranges, &neranges);
+
+	/* FIXME this should do neranges >= max_values */
+	if (neranges > 1)
+	{
+		/* build array of gap distances and sort them in ascending order */
+		distances = build_distances(distanceFn, ranges->colloid,
+									eranges, neranges);
+
+		/*
+		 * Combine ranges until we get below max_values. We don't use any
+		 * scale factor, because this is used during serialization, and we
+		 * don't expect more tuples to be inserted anytime soon.
+		 */
+		neranges = reduce_expanded_ranges(eranges, neranges, distances,
+										  max_values, cmpFn, ranges->colloid);
+
+		Assert(count_values(eranges, neranges) <= max_values);
+	}
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(ranges, eranges, neranges);
+
+	/* check all the range invariants */
+	AssertCheckRanges(ranges, cmpFn, ranges->colloid);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float		a1 = PG_GETARG_FLOAT4(0);
+	float		a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double		a1 = PG_GETARG_FLOAT8(0);
+	double		a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16		a1 = PG_GETARG_INT16(0);
+	int16		a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32		a1 = PG_GETARG_INT32(0);
+	int32		a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64		a1 = PG_GETARG_INT64(0);
+	int64		a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double		da1,
+				da2;
+
+	ItemPointer pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	/*
+	 * We use the no-check variants here, because user-supplied values may
+	 * have (ip_posid == 0). See ItemPointerCompare.
+	 */
+	da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa1);
+
+	da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Computes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum		d;
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int			i;
+	float8		delta = 0;
+
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN - 1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute approximate distance between two dates.
+ */
+Datum
+brin_minmax_multi_distance_date(PG_FUNCTION_ARGS)
+{
+	DateADT		dateVal1 = PG_GETARG_DATEADT(0);
+	DateADT		dateVal2 = PG_GETARG_DATEADT(1);
+
+	if (DATE_NOT_FINITE(dateVal1) || DATE_NOT_FINITE(dateVal2))
+		PG_RETURN_FLOAT8(0);
+
+	PG_RETURN_FLOAT8(dateVal1 - dateVal2);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeADT		ta = PG_GETARG_TIMEADT(0);
+	TimeADT		tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+Datum
+brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Timestamp	dt1 = PG_GETARG_TIMESTAMP(0);
+	Timestamp	dt2 = PG_GETARG_TIMESTAMP(1);
+
+	if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
+		PG_RETURN_FLOAT8(0);
+
+	delta = dt2 - dt1;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Interval   *ia = PG_GETARG_INTERVAL_P(0);
+	Interval   *ib = PG_GETARG_INTERVAL_P(1);
+	Interval   *result;
+
+	result = (Interval *) palloc(sizeof(Interval));
+
+	result->month = ib->month - ia->month;
+	/* overflow check copied from int4mi */
+	if (!SAMESIGN(ib->month, ia->month) &&
+		!SAMESIGN(result->month, ib->month))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->day = ib->day - ia->day;
+	if (!SAMESIGN(ib->day, ia->day) &&
+		!SAMESIGN(result->day, ib->day))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->time = ib->time - ia->time;
+	if (!SAMESIGN(ib->time, ia->time) &&
+		!SAMESIGN(result->time, ib->time))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	/*
+	 * We assume months have 31 days - we don't need to be precise, in the
+	 * worst case we'll build somewhat less efficient ranges.
+	 */
+	delta = (float8) (result->month * 31 + result->day);
+
+	/* convert to microseconds (just like the time part) */
+	delta = 24L * 3600L * delta;
+
+	/* and add the time part */
+	delta += result->time / (float8) 1000000.0;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((float8) b->h - (float8) a->h);
+	delta /= 256;
+
+	delta += ((float8) b->g - (float8) a->g);
+	delta /= 256;
+
+	delta += ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from different families are considered to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+	int			i;
+	int			len;
+	unsigned char *addra,
+			   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be in
+	 * maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		PG_RETURN_FLOAT8(1.0);
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16;				/* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len - 1; i >= 0; i--)
+	{
+		delta += (float8) addrb[i] - (float8) addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
+{
+	Ranges	   *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s;
+
+	/*
+	 * In batch mode, we need to compress the accumulated values to the
+	 * actually requested number of values/ranges.
+	 */
+	compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+
+	s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxOptions *opts)
+{
+	return MinMaxGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges	   *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, if possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 *
+	 * When starting with an empty range, we assume this is a batch mode, i.e.
+	 * we size the buffer for the maximum possible number of items in the
+	 * range (based on range size and max number of items on a page).
+	 *
+	 * XXX This may require quite a bit of memory, so maybe we should use some
+	 * value in between. OTOH most tables will have much wider rows, so the
+	 * number of rows per page is much lower.
+	 *
+	 * XXX Maybe we should do this (using larger buffer) even when there
+	 * already is a summary?
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		int			target_maxvalues;
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		/* what was specified as a reloption? */
+		target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, target_maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+		ranges = minmax_multi_init(maxvalues);
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+		ranges->target_maxvalues = target_maxvalues;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, serialized->maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		ranges = range_deserialize(maxvalues, serialized);
+
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+
+	/* MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS(); */
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges	   *ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized->maxvalues, serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum		minval = ranges->values[2 * rangeno];
+		Datum		maxval = ranges->values[2 * rangeno + 1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+					{
+						Datum		compar;
+						FmgrInfo   *cmpFn;
+
+						/* by default this range does not match */
+						matches = false;
+
+						/*
+						 * Otherwise, need to compare the new value with
+						 * boundaries of all the ranges. First check if it's
+						 * less than the absolute minimum, which is the first
+						 * value in the array.
+						 */
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTLessStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+						/* smaller than the smallest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTGreaterStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+						/* larger than the largest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						/*
+						 * haven't managed to eliminate this range, so
+						 * consider it matching
+						 */
+						matches = true;
+
+						break;
+					}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		val = ranges->values[2 * ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+
+	/* MinMaxOptions *opts = (MinMaxOptions *) PG_GET_OPCLASS_OPTIONS(); */
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges *serialized_a;
+	SerializedRanges *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	ExpandedRange *eranges;
+	int			neranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue *distances;
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a);
+	ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	neranges = (ranges_a->nranges + ranges_a->nvalues) +
+		(ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges with entries for the first range */
+	fill_expanded_ranges(eranges, ranges_a->nranges + ranges_a->nvalues,
+						 ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_expanded_ranges(&eranges[ranges_a->nranges + ranges_a->nvalues],
+						 ranges_b->nranges + ranges_b->nvalues,
+						 ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* sort the expanded ranges */
+	sort_expanded_ranges(cmpFn, colloid, eranges, neranges);
+
+	/*
+	 * We've loaded two different lists of expanded ranges, so some of them
+	 * may be overlapping. So walk through them and merge them.
+	 */
+	neranges = merge_overlapping_ranges(cmpFn, colloid, eranges, neranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidExpandedRanges(bdesc, colloid, attno, attr, eranges, neranges);
+
+	/*
+	 * If needed, reduce some of the ranges.
+	 *
+	 * XXX This may be fairly expensive, so maybe we should do it only when
+	 * it's actually needed (when we have too many ranges).
+	 */
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many of them to get below the threshold. The
+	 * collapsed ranges will be stored as a single value.
+	 *
+	 * XXX This does not apply the load factor, as we don't expect to add more
+	 * values to the range, so we prefer to keep as many ranges as possible.
+	 *
+	 * XXX Can the maxvalues be different in the two ranges? Perhaps we should
+	 * use maximum of those?
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  ranges_a->maxvalues,
+									  cmpFn, colloid);
+
+	/* update the first range summary */
+	store_expanded_ranges(ranges_a, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+								   uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							32, 8, 256, offsetof(MinMaxOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges	   *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges->maxvalues, ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum		a,
+					b;
+		text	   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum		a;
+		text	   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index a7eb1c9473..69ce6dfca4 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* if needed, serialize the values before forming the on-disk tuple */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(brdesc,
+												  tuple->bt_columns[keyno].bv_mem_value,
+												  tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -169,15 +177,15 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 datumno < brdesc->bd_info[keyno]->oi_nstored;
 			 datumno++)
 		{
-			Datum value = tuple->bt_columns[keyno].bv_values[datumno];
+			Datum		value = tuple->bt_columns[keyno].bv_values[datumno];
 
 #ifdef TOAST_INDEX_HACK
 
 			/* We must look at the stored type, not at the index descriptor. */
-			TypeCacheEntry	*atttype = brdesc->bd_info[keyno]->oi_typcache[datumno];
+			TypeCacheEntry *atttype = brdesc->bd_info[keyno]->oi_typcache[datumno];
 
 			/* Do we need to free the value at the end? */
-			bool free_value = false;
+			bool		free_value = false;
 
 			/* For non-varlena types we don't need to do anything special */
 			if (atttype->typlen != -1)
@@ -193,9 +201,9 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 * If value is stored EXTERNAL, must fetch it so we are not
 			 * depending on outside storage.
 			 *
-			 * XXX Is this actually true? Could it be that the summary is
-			 * NULL even for range with non-NULL data? E.g. degenerate bloom
-			 * filter may be thrown away, etc.
+			 * XXX Is this actually true? Could it be that the summary is NULL
+			 * even for range with non-NULL data? E.g. degenerate bloom filter
+			 * may be thrown away, etc.
 			 */
 			if (VARATT_IS_EXTERNAL(DatumGetPointer(value)))
 			{
@@ -205,8 +213,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			}
 
 			/*
-			 * If value is above size target, and is of a compressible datatype,
-			 * try to compress it in-line.
+			 * If value is above size target, and is of a compressible
+			 * datatype, try to compress it in-line.
 			 */
 			if (!VARATT_IS_EXTENDED(DatumGetPointer(value)) &&
 				VARSIZE(DatumGetPointer(value)) > TOAST_INDEX_TARGET &&
@@ -495,6 +503,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -574,6 +587,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 0e52d75457..9cd5fa9f62 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -24,6 +24,7 @@ typedef struct BrinOptions
 	bool		autosummarize;
 	double		nDistinctPerRange;	/* number of distinct values per range */
 	double		falsePositiveRate;	/* false positive for bloom filter */
+	int			valuesPerRange;		/* number of values per range */
 } BrinOptions;
 
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 8cc4e532e6..89254b5766 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -62,6 +62,9 @@ typedef struct BrinDesc
 	double		bd_nDistinctPerRange;
 	double		bd_falsePositiveRange;
 
+	/* parameters for minmax-multi indexes */
+	int			bd_valuesPerRange;
+
 	/* per-column info; bd_tupdesc->natts entries long */
 	BrinOpcInfo *bd_info[FLEXIBLE_ARRAY_MEMBER];
 } BrinDesc;
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..87de94f397 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,11 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+/*
+ * The BRIN opclasses may register serialization callback, in case the on-disk
+ * and in-memory representations differ (e.g. for performance reasons).
+ */
+typedef void (*brin_serialize_callback_type) (BrinDesc *bdesc, Datum src, Datum *dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +32,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 82e874130d..654584a03f 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 04d678f96a..8135854163 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2062,6 +2208,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2088,6 +2251,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2155,6 +2334,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2180,6 +2425,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2202,6 +2464,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2224,6 +2503,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2288,6 +2584,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2439,6 +2752,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2470,6 +2929,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2492,6 +2968,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2548,6 +3041,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2570,6 +3080,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2636,6 +3163,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 6709c8dfea..51403716b1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index dea9adaf98..ba9231ac8c 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '9926',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '9901',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '9927',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '9902',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '9928',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '9904',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '9929',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '9905',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '9930',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '9909',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '9910',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '9931',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '9932',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '9911',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '9933',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '9912',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '9934',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '9913',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '9935',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '9914',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '9936',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '9916',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '9937',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '9917',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '9938',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '9918',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '9939',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '9919',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 505120f963..8fe05aa2de 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8213,6 +8213,77 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '9940', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '9941', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '9942', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '9943', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '9944', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '9945', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '9946', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '9947', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '9948', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '9949', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '9950', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9951', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '9952', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '9953', descr => 'BRIN multi minmax date distance',
+  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
+{ oid => '9954', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '9955', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '9956', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '9957', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '9958', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '9959', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '9960', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+{ oid => '9961', descr => 'BRIN multi minmax timestamp distance',
+  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11437,4 +11508,18 @@
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
 
+{ oid => '9962', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '9963', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '9964', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '9965', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 74e279cbf9..e809094490 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -685,4 +685,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+{ oid => '9966',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..0a7e4b8060
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,450 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+ERROR:  value 7 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+DROP INDEX brinidx_multi;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- test building an index with many values, to force compaction of the buffer
+CREATE TABLE brin_large_range (a int4);
+INSERT INTO brin_large_range SELECT i FROM generate_series(1,10000) s(i);
+CREATE INDEX brin_large_range_idx ON brin_large_range USING brin (a int4_minmax_multi_ops);
+DROP TABLE brin_large_range;
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index e568b9fea2..0541c12a25 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  5017 | pg_mcv_list
  9925 | pg_brin_bloom_summary
-(5 rows)
+ 9966 | pg_brin_minmax_multi_summary
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 3dbea5b2cc..9d7a6a48ad 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index e5c873e0b4..7aa8957ad7 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..f5ffcaea33
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,403 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+
+DROP INDEX brinidx_multi;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- test building an index with many values, to force compaction of the buffer
+CREATE TABLE brin_large_range (a int4);
+INSERT INTO brin_large_range SELECT i FROM generate_series(1,10000) s(i);
+CREATE INDEX brin_large_range_idx ON brin_large_range USING brin (a int4_minmax_multi_ops);
+DROP TABLE brin_large_range;
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.26.2

0007-Ignore-correlation-for-new-BRIN-opclasses-20210311.patchtext/x-patch; charset=UTF-8; name=0007-Ignore-correlation-for-new-BRIN-opclasses-20210311.patchDownload
From 2e113f7041f4cfa1fa7d879c6260c23df20b59f8 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 12 Sep 2020 15:07:59 +0200
Subject: [PATCH 7/8] Ignore correlation for new BRIN opclasses

The new BRIN opclasses (bloom and minmax-multi) are less sensitive to
poorly correlated data, so just assume the data is perfectly correlated
during costing.

Author: Tomas Vondra <tomas.vondra@2ndquadrant.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin_bloom.c        |  1 +
 src/backend/access/brin/brin_minmax_multi.c |  1 +
 src/backend/utils/adt/selfuncs.c            | 19 ++++++++++++++++++-
 src/include/access/brin_internal.h          |  3 +++
 4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index c086e83236..22590b2351 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -412,6 +412,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(BloomOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index d19ebf08c2..6957be0bdb 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1737,6 +1737,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
 					 sizeof(MinmaxMultiOpaque));
+	result->oi_ignore_correlation = true;
 	result->oi_nstored = 1;
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 52314d3aa1..0320d128f6 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -98,6 +98,7 @@
 #include <math.h>
 
 #include "access/brin.h"
+#include "access/brin_internal.h"
 #include "access/brin_page.h"
 #include "access/gin.h"
 #include "access/table.h"
@@ -7352,7 +7353,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		minimalRanges;
 	double		estimatedRanges;
 	double		selec;
-	Relation	indexRel;
+	Relation	indexRel = NULL;
+	TupleDesc	tupdesc = NULL;
 	ListCell   *l;
 	VariableStatData vardata;
 
@@ -7374,6 +7376,7 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 */
 		indexRel = index_open(index->indexoid, NoLock);
 		brinGetStats(indexRel, &statsData);
+		tupdesc = RelationGetDescr(indexRel);
 		index_close(indexRel, NoLock);
 
 		/* work out the actual number of ranges in the index */
@@ -7407,6 +7410,17 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, l);
 		AttrNumber	attnum = index->indexkeys[iclause->indexcol];
+		FmgrInfo   *opcInfoFn;
+		BrinOpcInfo *opcInfo;
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, iclause->indexcol);
+		bool		ignore_correlation;
+
+		opcInfoFn = index_getprocinfo(indexRel, iclause->indexcol + 1, BRIN_PROCNUM_OPCINFO);
+
+		opcInfo = (BrinOpcInfo *)
+			DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid));
+
+		ignore_correlation = opcInfo->oi_ignore_correlation;
 
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
@@ -7477,6 +7491,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (sslot.nnumbers > 0)
 					varCorrelation = Abs(sslot.numbers[0]);
 
+				if (ignore_correlation)
+					varCorrelation = 1.0;
+
 				if (varCorrelation > *indexCorrelation)
 					*indexCorrelation = varCorrelation;
 
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 89254b5766..063b703208 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -30,6 +30,9 @@ typedef struct BrinOpcInfo
 	/* Regular processing of NULLs in BrinValues? */
 	bool		oi_regular_nulls;
 
+	/* Ignore correlation during cost estimation */
+	bool		oi_ignore_correlation;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.26.2

0008-Consider-selectivity-in-choose_bitmap_and-20210311.patchtext/x-patch; charset=UTF-8; name=0008-Consider-selectivity-in-choose_bitmap_and-20210311.patchDownload
From 66fe5793a8545d59a2ee8e4059ce1d1fdbcb0272 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Thu, 11 Mar 2021 12:57:51 +0100
Subject: [PATCH 8/8] Consider selectivity in choose_bitmap_and

---
 src/backend/optimizer/path/indxpath.c | 38 ++++++++++++++++-----------
 1 file changed, 23 insertions(+), 15 deletions(-)

diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index ff536e6b24..36b29c6c7b 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -1437,6 +1437,7 @@ choose_bitmap_and(PlannerInfo *root, RelOptInfo *rel, List *paths)
 	foreach(l, paths)
 	{
 		Path	   *ipath = (Path *) lfirst(l);
+		bool		duplicate = false;
 
 		pathinfo = classify_index_clause_usage(ipath, &clauselist);
 
@@ -1451,22 +1452,29 @@ choose_bitmap_and(PlannerInfo *root, RelOptInfo *rel, List *paths)
 		{
 			if (!pathinfoarray[i]->unclassifiable &&
 				bms_equal(pathinfo->clauseids, pathinfoarray[i]->clauseids))
-				break;
-		}
-		if (i < npaths)
-		{
-			/* duplicate clauseids, keep the cheaper one */
-			Cost		ncost;
-			Cost		ocost;
-			Selectivity nselec;
-			Selectivity oselec;
-
-			cost_bitmap_tree_node(pathinfo->path, &ncost, &nselec);
-			cost_bitmap_tree_node(pathinfoarray[i]->path, &ocost, &oselec);
-			if (ncost < ocost)
-				pathinfoarray[i] = pathinfo;
+			{
+				/* duplicate clauseids, keep the cheaper one
+				 *
+				 * XXX maybe this should just use path_usage_comparator?
+				 */
+				Cost		ncost;
+				Cost		ocost;
+				Selectivity nselec;
+				Selectivity oselec;
+
+				cost_bitmap_tree_node(pathinfo->path, &ncost, &nselec);
+				cost_bitmap_tree_node(pathinfoarray[i]->path, &ocost, &oselec);
+
+				if ((ncost < ocost) && (nselec < oselec))
+				{
+					pathinfoarray[i] = pathinfo;
+					duplicate = true;
+					break;
+				}
+			}
 		}
-		else
+
+		if (!duplicate)
 		{
 			/* not duplicate clauseids, add to array */
 			pathinfoarray[npaths++] = pathinfo;
-- 
2.26.2

reproduce2.sqlapplication/sql; name=reproduce2.sqlDownload
reproducer.sqlapplication/sql; name=reproducer.sqlDownload
#161John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#160)
Re: WIP: BRIN multi-range indexes

On Thu, Mar 11, 2021 at 12:26 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

Hi,

Here is an updated version of the patch series.

It fixes the assert failure (just remove the multiplication from it) and
adds a simple regression test that exercises this.

Based on the discussion so far, I've decided to keep just the new
signature of the consistent function. That's a bit simpler than having
to support both 3 and 4 parameters, and it would not deal with the NULL
changes anyway (mostly harmless code duplication, but still). I've also
realized the API documentation in SGML needs updating.

At this point, I think 0001-0006 parts are mostly committable.

I think so. I've marked it RFC for this six.

As for the remaining two parts, the one dealing with correlation may not
be strictly necessary, but not having it (or something like it) may
result in not picking the BRIN index in some cases.

But maybe it's not a major problem. I tried the example from [1] but it
no longer triggers the issue for me - I'm not entirely sure why, but the
costing changed for some reason. It used to look like this:

...

The index scan cost is about the same, but the heap scan is about half
the cost. The row estimates are a bit different, perhaps that's related.
The seqscan cost (169248) and duration (~500ms) is still about the same,
but so now we still pick the bitmap heap scan.

With only 0001-0006, I get a parallel bitmap scan in both cases:

QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------
Gather (cost=6542.42..52779.35 rows=10 width=4) (actual
time=3.283..22.308 rows=10 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Bitmap Heap Scan on t0 (cost=5542.42..51778.35 rows=4
width=4) (actual time=3.434..17.257 rows=3 loops=3)
Recheck Cond: (a = 10000)
Rows Removed by Index Recheck: 83165
Heap Blocks: lossy=421
-> Bitmap Index Scan on t0_a_idx (cost=0.00..5542.42 rows=381682
width=0) (actual time=2.996..2.996 rows=11040 loops=1)
Index Cond: (a = 10000)
Planning Time: 0.088 ms
Execution Time: 22.341 ms
(11 rows)

Not sure we can rely on
this, though. Would be quite sad if we had new improved opclasses but
the planner often decided not to use them.

Yeah, I'm not sure what to do here. It might be okay to leave it out now
and study it further as a PG14 open item or PG15 improvement.

I had an idea of tweaking choose_bitmap_and to consider both the cost
and selectivity (similarly to how add_path considers statup/total cost),
and that did indeed resolve this particular case. This is what the 0008
part does.

But it may also have negative consequence, as demonstrated by the
reproducer2.sql script. So maybe the logic would need to be more
complicated. Or maybe there's no reliable solution, considering how
tricky/unreliable BRIN estimates are.

Ok, so with 0008 in reproducer2, it chooses the more selective path, even
though it has a higher total cost:

0001-0007:

QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on t2 (cost=29.03..24032.28 rows=1 width=8) (actual
time=0.498..1.755 rows=1 loops=1)
Recheck Cond: (a = 1000)
Rows Removed by Index Recheck: 7167
Heap Blocks: lossy=128
-> Bitmap Index Scan on idx_2 (cost=0.00..29.03 rows=7163 width=0)
(actual time=0.278..0.278 rows=1280 loops=1)
Index Cond: (a = 1000)
Planning Time: 0.148 ms
Execution Time: 1.774 ms
(8 rows)

DROP INDEX
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on t2 (cost=656.00..1531.00 rows=1 width=8) (actual
time=9.695..9.708 rows=1 loops=1)
Recheck Cond: (a = 1000)
Rows Removed by Index Recheck: 223
Heap Blocks: lossy=4
-> Bitmap Index Scan on idx_1 (cost=0.00..656.00 rows=224 width=0)
(actual time=9.675..9.675 rows=40 loops=1)
Index Cond: (a = 1000)
Planning Time: 0.110 ms
Execution Time: 9.727 ms
(8 rows)

and with 0008:

QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on t2 (cost=656.00..1531.00 rows=1 width=8) (actual
time=8.540..8.577 rows=1 loops=1)
Recheck Cond: (a = 1000)
Rows Removed by Index Recheck: 223
Heap Blocks: lossy=4
-> Bitmap Index Scan on idx_1 (cost=0.00..656.00 rows=224 width=0)
(actual time=8.507..8.508 rows=40 loops=1)
Index Cond: (a = 1000)
Planning Time: 0.175 ms
Execution Time: 8.601 ms
(8 rows)

DROP INDEX
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on t2 (cost=656.00..1531.00 rows=1 width=8) (actual
time=9.712..9.725 rows=1 loops=1)
Recheck Cond: (a = 1000)
Rows Removed by Index Recheck: 223
Heap Blocks: lossy=4
-> Bitmap Index Scan on idx_1 (cost=0.00..656.00 rows=224 width=0)
(actual time=9.691..9.692 rows=40 loops=1)
Index Cond: (a = 1000)
Planning Time: 0.104 ms
Execution Time: 9.746 ms
(8 rows)

That being said, I don't think this is something we need to solve here,
and it may not actually be an issue at all. For this to happen there
need to be a terrible index on the same attribute (like the minmax index
in the example above). But why keeping such index anyway? Dropping it
would make the issue go away. If we have two indexes that both perform
reasonably (say, bloom and minmax-multi), the consequences are not that
bad. so this is interesting, but probably fine.

Yeah, I suspect this is unlikely to be a problem in practice.

--
I've run a similar test based on an earlier example from some months ago
(attached).

0001-0006:

At first regular BRIN is faster, and it will choose it if available:

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on iot (cost=404.79..233352.86 rows=1252175 width=57)
(actual time=2.115..346.351 rows=1252800 loops=1)
Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with time
zone))
Rows Removed by Index Recheck: 15936
Heap Blocks: lossy=15104
-> Bitmap Index Scan on cd_multi (cost=0.00..91.74 rows=1256702
width=0) (actual time=0.972..0.972 rows=151040 loops=1)
Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
Planning Time: 0.208 ms
Execution Time: 412.549 ms
(8 rows)

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on iot (cost=341.99..233335.81 rows=1256871 width=57)
(actual time=1.244..170.962 rows=1252800 loops=1)
Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with time
zone))
Rows Removed by Index Recheck: 15936
Heap Blocks: lossy=15104
-> Bitmap Index Scan on cd_single (cost=0.00..27.78 rows=1267458
width=0) (actual time=0.406..0.406 rows=151040 loops=1)
Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
Planning Time: 0.197 ms
Execution Time: 237.146 ms
(8 rows)

After delete, vacuum, and insert, BRIN multi is chosen over seq scan even
though the correlation should be somewhat off (I didn't go further and try
to find a case where seq scan is wrongly preferred):

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on iot (cost=428.53..247273.68 rows=1135074 width=57)
(actual time=1.798..252.494 rows=1128038 loops=1)
Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with time
zone))
Rows Removed by Index Recheck: 140698
Heap Blocks: lossy=15104
-> Bitmap Index Scan on cd_multi (cost=0.00..144.76 rows=1598833
width=0) (actual time=0.940..0.941 rows=151040 loops=1)
Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
Planning Time: 0.152 ms
Execution Time: 311.999 ms
(8 rows)

Add regular BRIN index, and it will get chosen, making the scan slower:

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on iot (cost=308.20..246966.38 rows=1118304 width=57)
(actual time=5.685..1277.854 rows=1128038 loops=1)
Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with time
zone))
Rows Removed by Index Recheck: 7548826
Heap Blocks: lossy=103296
-> Bitmap Index Scan on cd_single (cost=0.00..28.62 rows=1551397
width=0) (actual time=4.609..4.609 rows=1032960 loops=1)
Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
Planning Time: 0.211 ms
Execution Time: 1338.685 ms
(8 rows)

Apply 0007 -- no difference:

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on iot (cost=308.20..246966.38 rows=1118304 width=57)
(actual time=6.988..1358.113 rows=1128038 loops=1)
Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with time
zone))
Rows Removed by Index Recheck: 7548826
Heap Blocks: lossy=103296
-> Bitmap Index Scan on cd_single (cost=0.00..28.62 rows=1551397
width=0) (actual time=5.528..5.528 rows=1032960 loops=1)
Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
Planning Time: 3.534 ms
Execution Time: 1418.194 ms
(8 rows)

Apply 0008 -- now it chooses minmax-multi:

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on iot (cost=422.94..245412.66 rows=1118304 width=57)
(actual time=2.750..259.850 rows=1128038 loops=1)
Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with time
zone))
Rows Removed by Index Recheck: 140698
Heap Blocks: lossy=15104
-> Bitmap Index Scan on cd_multi (cost=0.00..143.36 rows=1128092
width=0) (actual time=1.609..1.609 rows=151040 loops=1)
Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
Planning Time: 1.421 ms
Execution Time: 319.891 ms
(8 rows)

So, 0007 doesn't make a difference in this case, but 0008 does.

--
John Naylor
EDB: http://www.enterprisedb.com

#162Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#161)
Re: WIP: BRIN multi-range indexes

On 3/17/21 7:59 PM, John Naylor wrote:

On Thu, Mar 11, 2021 at 12:26 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

Hi,

Here is an updated version of the patch series.

It fixes the assert failure (just remove the multiplication from it) and
adds a simple regression test that exercises this.

Based on the discussion so far, I've decided to keep just the new
signature of the consistent function. That's a bit simpler than having
to support both 3 and 4 parameters, and it would not deal with the NULL
changes anyway (mostly harmless code duplication, but still). I've also
realized the API documentation in SGML needs updating.

At this point, I think 0001-0006 parts are mostly committable.

I think so. I've marked it RFC for this six.

As for the remaining two parts, the one dealing with correlation may not
be strictly necessary, but not having it (or something like it) may
result in not picking the BRIN index in some cases.

But maybe it's not a major problem. I tried the example from [1] but it
no longer triggers the issue for me - I'm not entirely sure why, but the
costing changed for some reason. It used to look like this:

...

The index scan cost is about the same, but the heap scan is about half
the cost. The row estimates are a bit different, perhaps that's related.
The seqscan cost (169248) and duration (~500ms) is still about the same,
but so now we still pick the bitmap heap scan.

With only 0001-0006, I get a parallel bitmap scan in both cases:

                                                            QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=6542.42..52779.35 rows=10 width=4) (actual
time=3.283..22.308 rows=10 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->  Parallel Bitmap Heap Scan on t0  (cost=5542.42..51778.35 rows=4
width=4) (actual time=3.434..17.257 rows=3 loops=3)
         Recheck Cond: (a = 10000)
         Rows Removed by Index Recheck: 83165
         Heap Blocks: lossy=421
         ->  Bitmap Index Scan on t0_a_idx  (cost=0.00..5542.42
rows=381682 width=0) (actual time=2.996..2.996 rows=11040 loops=1)
               Index Cond: (a = 10000)
 Planning Time: 0.088 ms
 Execution Time: 22.341 ms
(11 rows)

Not sure we can rely on
this, though. Would be quite sad if we had new improved opclasses but
the planner often decided not to use them.

Yeah, I'm not sure what to do here. It might be okay to leave it out now
and study it further as a PG14 open item or PG15 improvement.

Yeah, that's definitely an option.

I had an idea of tweaking choose_bitmap_and to consider both the cost
and selectivity (similarly to how add_path considers statup/total cost),
and that did indeed resolve this particular case. This is what the 0008
part does.

But it may also have negative consequence, as demonstrated by the
reproducer2.sql script. So maybe the logic would need to be more
complicated. Or maybe there's no reliable solution, considering how
tricky/unreliable BRIN estimates are.

Ok, so with 0008 in reproducer2, it chooses the more selective path,
even though it has a higher total cost:

0001-0007:

                                                     QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on t2  (cost=29.03..24032.28 rows=1 width=8) (actual
time=0.498..1.755 rows=1 loops=1)
   Recheck Cond: (a = 1000)
   Rows Removed by Index Recheck: 7167
   Heap Blocks: lossy=128
   ->  Bitmap Index Scan on idx_2  (cost=0.00..29.03 rows=7163 width=0)
(actual time=0.278..0.278 rows=1280 loops=1)
         Index Cond: (a = 1000)
 Planning Time: 0.148 ms
 Execution Time: 1.774 ms
(8 rows)

DROP INDEX
                                                    QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on t2  (cost=656.00..1531.00 rows=1 width=8) (actual
time=9.695..9.708 rows=1 loops=1)
   Recheck Cond: (a = 1000)
   Rows Removed by Index Recheck: 223
   Heap Blocks: lossy=4
   ->  Bitmap Index Scan on idx_1  (cost=0.00..656.00 rows=224 width=0)
(actual time=9.675..9.675 rows=40 loops=1)
         Index Cond: (a = 1000)
 Planning Time: 0.110 ms
 Execution Time: 9.727 ms
(8 rows)

and with 0008:

                                                    QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on t2  (cost=656.00..1531.00 rows=1 width=8) (actual
time=8.540..8.577 rows=1 loops=1)
   Recheck Cond: (a = 1000)
   Rows Removed by Index Recheck: 223
   Heap Blocks: lossy=4
   ->  Bitmap Index Scan on idx_1  (cost=0.00..656.00 rows=224 width=0)
(actual time=8.507..8.508 rows=40 loops=1)
         Index Cond: (a = 1000)
 Planning Time: 0.175 ms
 Execution Time: 8.601 ms
(8 rows)

DROP INDEX
                                                    QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on t2  (cost=656.00..1531.00 rows=1 width=8) (actual
time=9.712..9.725 rows=1 loops=1)
   Recheck Cond: (a = 1000)
   Rows Removed by Index Recheck: 223
   Heap Blocks: lossy=4
   ->  Bitmap Index Scan on idx_1  (cost=0.00..656.00 rows=224 width=0)
(actual time=9.691..9.692 rows=40 loops=1)
         Index Cond: (a = 1000)
 Planning Time: 0.104 ms
 Execution Time: 9.746 ms
(8 rows)

That being said, I don't think this is something we need to solve here,
and it may not actually be an issue at all. For this to happen there
need to be a terrible index on the same attribute (like the minmax index
in the example above). But why keeping such index anyway? Dropping it
would make the issue go away. If we have two indexes that both perform
reasonably (say, bloom and minmax-multi), the consequences are not that
bad. so this is interesting, but probably fine.

Yeah, I suspect this is unlikely to be a problem in practice.

--
I've run a similar test based on an earlier example from some months ago
(attached).

Ummm, no attachment ;-)

0001-0006:

At first regular BRIN is faster, and it will choose it if available:

                                                                       
  QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on iot  (cost=404.79..233352.86 rows=1252175 width=57)
(actual time=2.115..346.351 rows=1252800 loops=1)
   Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
   Rows Removed by Index Recheck: 15936
   Heap Blocks: lossy=15104
   ->  Bitmap Index Scan on cd_multi  (cost=0.00..91.74 rows=1256702
width=0) (actual time=0.972..0.972 rows=151040 loops=1)
         Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp
with time zone))
 Planning Time: 0.208 ms
 Execution Time: 412.549 ms
(8 rows)

                                                                       
  QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on iot  (cost=341.99..233335.81 rows=1256871 width=57)
(actual time=1.244..170.962 rows=1252800 loops=1)
   Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
   Rows Removed by Index Recheck: 15936
   Heap Blocks: lossy=15104
   ->  Bitmap Index Scan on cd_single  (cost=0.00..27.78 rows=1267458
width=0) (actual time=0.406..0.406 rows=151040 loops=1)
         Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp
with time zone))
 Planning Time: 0.197 ms
 Execution Time: 237.146 ms
(8 rows)

After delete, vacuum, and insert, BRIN multi is chosen over seq scan
even though the correlation should be somewhat off (I didn't go further
and try to find a case where seq scan is wrongly preferred):

                                                                       
  QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on iot  (cost=428.53..247273.68 rows=1135074 width=57)
(actual time=1.798..252.494 rows=1128038 loops=1)
   Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
   Rows Removed by Index Recheck: 140698
   Heap Blocks: lossy=15104
   ->  Bitmap Index Scan on cd_multi  (cost=0.00..144.76 rows=1598833
width=0) (actual time=0.940..0.941 rows=151040 loops=1)
         Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp
with time zone))
 Planning Time: 0.152 ms
 Execution Time: 311.999 ms
(8 rows)

Add regular BRIN index, and it will get chosen, making the scan slower:

                                                                       
  QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on iot  (cost=308.20..246966.38 rows=1118304 width=57)
(actual time=5.685..1277.854 rows=1128038 loops=1)
   Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
   Rows Removed by Index Recheck: 7548826
   Heap Blocks: lossy=103296
   ->  Bitmap Index Scan on cd_single  (cost=0.00..28.62 rows=1551397
width=0) (actual time=4.609..4.609 rows=1032960 loops=1)
         Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp
with time zone))
 Planning Time: 0.211 ms
 Execution Time: 1338.685 ms
(8 rows)

Apply 0007 -- no difference:

                                                                       
  QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on iot  (cost=308.20..246966.38 rows=1118304 width=57)
(actual time=6.988..1358.113 rows=1128038 loops=1)
   Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
   Rows Removed by Index Recheck: 7548826
   Heap Blocks: lossy=103296
   ->  Bitmap Index Scan on cd_single  (cost=0.00..28.62 rows=1551397
width=0) (actual time=5.528..5.528 rows=1032960 loops=1)
         Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp
with time zone))
 Planning Time: 3.534 ms
 Execution Time: 1418.194 ms
(8 rows)

Apply 0008 -- now it chooses minmax-multi:

                                                                       
  QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on iot  (cost=422.94..245412.66 rows=1118304 width=57)
(actual time=2.750..259.850 rows=1128038 loops=1)
   Recheck Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp with
time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp with
time zone))
   Rows Removed by Index Recheck: 140698
   Heap Blocks: lossy=15104
   ->  Bitmap Index Scan on cd_multi  (cost=0.00..143.36 rows=1128092
width=0) (actual time=1.609..1.609 rows=151040 loops=1)
         Index Cond: ((create_dt >= '2020-02-01 00:00:00-04'::timestamp
with time zone) AND (create_dt < '2020-03-01 00:00:00-04'::timestamp
with time zone))
 Planning Time: 1.421 ms
 Execution Time: 319.891 ms
(8 rows)

So, 0007 doesn't make a difference in this case, but 0008 does.

Interesting. Anyway, we picked the BRIN in both cases (over just using
seqscan), and it's unlikely to have two brin indexes with different
opclasses.

Barring objections, I'll get the 0001-0006 parts committed soon, and
then we can continue working on the costing changes for the next major
version.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#163John Naylor
john.naylor@enterprisedb.com
In reply to: Tomas Vondra (#162)
1 attachment(s)
Re: WIP: BRIN multi-range indexes

On Wed, Mar 17, 2021 at 3:16 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

Ummm, no attachment ;-)

Oops, here it is.

--
John Naylor
EDB: http://www.enterprisedb.com

Attachments:

jcn-costing-test.sqlapplication/octet-stream; name=jcn-costing-test.sqlDownload
#164Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: John Naylor (#161)
8 attachment(s)
Re: WIP: BRIN multi-range indexes

On 3/17/21 7:59 PM, John Naylor wrote:

On Thu, Mar 11, 2021 at 12:26 PM Tomas Vondra
<tomas.vondra@enterprisedb.com <mailto:tomas.vondra@enterprisedb.com>>
wrote:

Hi,

Here is an updated version of the patch series.

It fixes the assert failure (just remove the multiplication from it) and
adds a simple regression test that exercises this.

Based on the discussion so far, I've decided to keep just the new
signature of the consistent function. That's a bit simpler than having
to support both 3 and 4 parameters, and it would not deal with the NULL
changes anyway (mostly harmless code duplication, but still). I've also
realized the API documentation in SGML needs updating.

At this point, I think 0001-0006 parts are mostly committable.

I think so. I've marked it RFC for this six.

I was just about to commit the 0001-0006 over the weekend. I went over
the patches to polish them, most of the changes were pretty simple:

- minor cleanup / rewording of comments

- resolving two remaining FIXMEs in the minmax-multi patch

- removing/rewording a bunch of XXX comments (most of them are about
possible future improvements)

- assigned proper OIDs and bumped catversion in patches touching the
catalogs

- bugfix: 0005 and 0006 were still adding fields into BrinOptions and
BrinDesc, but that was used before we got opclass parameters

- bugfix: two or three corrections in catalog contents

- doc fix: the brin.sgml bloom was referring to bit_bloom_ops, but
there's no such thing

- I have considered to get rid of 0004 (essentially merging it into 0002
and 0003 patches) but I decided not to do that, as it'd make the changes
in those two patches less obvious.

But then I remembered that long time ago there were suggestions to not
include the new opclasses directly, but through contrib. I'm not sure if
we want to do that, but I decided to give it a try - attached are the
polished patches 0001-0006, along with two patches that move the bloom
and minmax-multi opclasses to contrib.

In general, it seems much easier to define the opclasses in extension as
opposed to doing it directly in src/include/catalog. It however probably
needs a bit more work - in particular, the extensions currently create
just operator classes, not operator families. Not sure what consequences
could that have, though.

All the regression tests work fine, with the exception of minmax-multi
on a CIDR column. I don't know why, but the CREATE INDEX then fails like
this:

ERROR: missing operator 1(650,650) in opfamily 16463

650 is cidr, so this essentially says there's no (cidr<cidr) operator.
With the opclasses defined in .dat files this however worked, so I
suppose it's related to the missing operator families.

There's one remaining problem, though - the opclasses are using custom
data types for the range summaries. Essentially a varlena data type,
with internal structure. When defined directly in core, we knew the OID
of that type, which allowed us to store it into BrinOpcInfo. That's
mostly internal information, but pageinspect used it to print the
summary in brin_page_items (by just using the type out function).
Obviously, without the OID it prints just bytea blob, which is not very
useful :-(

I wonder if we could lookup the type, somehow. I mean, we know the name
of the types (brin_bloom_summary/brin_minmax_multi_summary), so perhaps
we could look it up in TYPENAMENSP? Not sure where to get the namespace,
though, and maybe there are (security) issues with this? Or maybe we
could pass the type OID to pageinspect directly (we're already passing
OID of the index, so we already have to trust the value).

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-Move-bsearch_arg-to-src-port-20210322.patchtext/x-patch; charset=UTF-8; name=0001-Move-bsearch_arg-to-src-port-20210322.patchDownload
From 05f39aeffcb7d9084b552c4902faf158a94431cd Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Mon, 22 Mar 2021 00:24:21 +0100
Subject: [PATCH 1/8] Move bsearch_arg to src/port

Until now the bsearch_arg function was used only in extended statistics
code, so it was defined in that code.  But we already have qsort_arg in
src/port, so let's move it next to it.
---
 src/backend/statistics/extended_stats.c       | 31 --------
 src/include/port.h                            |  5 ++
 .../statistics/extended_stats_internal.h      |  5 --
 src/port/Makefile                             |  1 +
 src/port/bsearch_arg.c                        | 78 +++++++++++++++++++
 src/tools/msvc/Mkvcbuild.pm                   |  2 +-
 6 files changed, 85 insertions(+), 37 deletions(-)
 create mode 100644 src/port/bsearch_arg.c

diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index a030ea3653..fa42851fd5 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -659,37 +659,6 @@ compare_datums_simple(Datum a, Datum b, SortSupport ssup)
 	return ApplySortComparator(a, false, b, false, ssup);
 }
 
-/* simple counterpart to qsort_arg */
-void *
-bsearch_arg(const void *key, const void *base, size_t nmemb, size_t size,
-			int (*compar) (const void *, const void *, void *),
-			void *arg)
-{
-	size_t		l,
-				u,
-				idx;
-	const void *p;
-	int			comparison;
-
-	l = 0;
-	u = nmemb;
-	while (l < u)
-	{
-		idx = (l + u) / 2;
-		p = (void *) (((const char *) base) + (idx * size));
-		comparison = (*compar) (key, p, arg);
-
-		if (comparison < 0)
-			u = idx;
-		else if (comparison > 0)
-			l = idx + 1;
-		else
-			return (void *) p;
-	}
-
-	return NULL;
-}
-
 /*
  * build_attnums_array
  *		Transforms a bitmap into an array of AttrNumber values.
diff --git a/src/include/port.h b/src/include/port.h
index 227ef4b148..82f63de325 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -508,6 +508,11 @@ typedef int (*qsort_arg_comparator) (const void *a, const void *b, void *arg);
 extern void qsort_arg(void *base, size_t nel, size_t elsize,
 					  qsort_arg_comparator cmp, void *arg);
 
+extern void *bsearch_arg(const void *key, const void *base,
+						 size_t nmemb, size_t size,
+						 int (*compar) (const void *, const void *, void *),
+						 void *arg);
+
 /* port/chklocale.c */
 extern int	pg_get_encoding_from_locale(const char *ctype, bool write_message);
 
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index c849bd57c0..a0a3cf5b0f 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -85,11 +85,6 @@ extern int	multi_sort_compare_dims(int start, int end, const SortItem *a,
 extern int	compare_scalars_simple(const void *a, const void *b, void *arg);
 extern int	compare_datums_simple(Datum a, Datum b, SortSupport ssup);
 
-extern void *bsearch_arg(const void *key, const void *base,
-						 size_t nmemb, size_t size,
-						 int (*compar) (const void *, const void *, void *),
-						 void *arg);
-
 extern AttrNumber *build_attnums_array(Bitmapset *attrs, int *numattrs);
 
 extern SortItem *build_sorted_items(int numrows, int *nitems, HeapTuple *rows,
diff --git a/src/port/Makefile b/src/port/Makefile
index e41b005c4f..52dbf5783f 100644
--- a/src/port/Makefile
+++ b/src/port/Makefile
@@ -40,6 +40,7 @@ LIBS += $(PTHREAD_LIBS)
 OBJS = \
 	$(LIBOBJS) \
 	$(PG_CRC32C_OBJS) \
+	bsearch_arg.o \
 	chklocale.o \
 	erand48.o \
 	inet_net_ntop.o \
diff --git a/src/port/bsearch_arg.c b/src/port/bsearch_arg.c
new file mode 100644
index 0000000000..0f1eaeba83
--- /dev/null
+++ b/src/port/bsearch_arg.c
@@ -0,0 +1,78 @@
+/*
+ * bsearch_arg.c: bsearch variant with a user-supplied pointer
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ * Copyright (c) 1990 Regents of the University of California.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. [rescinded 22 July 1999]
+ * 4. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * src/port/bsearch_arg.c
+ */
+
+#include "c.h"
+
+/*
+ * Perform a binary search.
+ *
+ * The code below is a bit sneaky.  After a comparison fails, we
+ * divide the work in half by moving either left or right. If lim
+ * is odd, moving left simply involves halving lim: e.g., when lim
+ * is 5 we look at item 2, so we change lim to 2 so that we will
+ * look at items 0 & 1.  If lim is even, the same applies.  If lim
+ * is odd, moving right again involes halving lim, this time moving
+ * the base up one item past p: e.g., when lim is 5 we change base
+ * to item 3 and make lim 2 so that we will look at items 3 and 4.
+ * If lim is even, however, we have to shrink it by one before
+ * halving: e.g., when lim is 4, we still looked at item 2, so we
+ * have to make lim 3, then halve, obtaining 1, so that we will only
+ * look at item 3.
+ */
+void *
+bsearch_arg(const void *key, const void *base0,
+			size_t nmemb, size_t size,
+			int (*compar) (const void *, const void *, void *),
+			void *arg)
+{
+	const char *base = (const char *) base0;
+	int			lim,
+				cmp;
+	const void *p;
+
+	for (lim = nmemb; lim != 0; lim >>= 1)
+	{
+		p = base + (lim >> 1) * size;
+		cmp = (*compar) (key, p, arg);
+		if (cmp == 0)
+			return (void *) p;
+		if (cmp > 0)
+		{						/* key > p: move right */
+			base = (const char *) p + size;
+			lim--;
+		}						/* else move left */
+	}
+	return (NULL);
+}
diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm
index a184404e21..bc65185130 100644
--- a/src/tools/msvc/Mkvcbuild.pm
+++ b/src/tools/msvc/Mkvcbuild.pm
@@ -103,7 +103,7 @@ sub mkvcbuild
 	  dirent.c dlopen.c getopt.c getopt_long.c link.c
 	  pread.c preadv.c pwrite.c pwritev.c pg_bitutils.c
 	  pg_strong_random.c pgcheckdir.c pgmkdirp.c pgsleep.c pgstrcasecmp.c
-	  pqsignal.c mkdtemp.c qsort.c qsort_arg.c quotes.c system.c
+	  pqsignal.c mkdtemp.c qsort.c qsort_arg.c bsearch_arg.c quotes.c system.c
 	  strerror.c tar.c thread.c
 	  win32env.c win32error.c win32security.c win32setlocale.c win32stat.c);
 
-- 
2.30.2

0002-Pass-all-scan-keys-to-BRIN-consistent-funct-20210322.patchtext/x-patch; charset=UTF-8; name=0002-Pass-all-scan-keys-to-BRIN-consistent-funct-20210322.patchDownload
From feee736d600805956cedb7b9d80b637dd6b4ab98 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Mon, 22 Mar 2021 00:24:26 +0100
Subject: [PATCH 2/8] Pass all scan keys to BRIN consistent function at once

Passing all scan keys to the BRIN consistent function at once may allow
elimination of additional ranges, which would be impossible when only
passing individual scan keys.

This modifies the existing BRIN opclasses (minmax, inclusion) although
those don't really benefit from this change. The primary purpose of this
is to allow future opclases to benefit from seeing all keys at once.

This does break existing BRIN opclasses, because the signature of the
consistent function changed. We might support both variants (with 3 and
4 parameters), but that seems pointless.  The opclasses have to be
updated because of the changes to NULL handling anyway, and we're not
aware of any out-of-core BRIN opclasses.  There may be some, but it's
not worth the extra complexity.

Bump catversion, because of pg_proc changes.

Author: Tomas Vondra <tomas.vondra@postgresql.org>
Reviewed-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 doc/src/sgml/brin.sgml                   |   8 +-
 src/backend/access/brin/brin.c           | 116 +++++++++++++------
 src/backend/access/brin/brin_inclusion.c | 140 ++++++++++++++++-------
 src/backend/access/brin/brin_minmax.c    |  92 ++++++++++++---
 src/backend/access/brin/brin_validate.c  |   4 +-
 src/include/catalog/catversion.h         |   2 +-
 src/include/catalog/pg_proc.dat          |   4 +-
 7 files changed, 268 insertions(+), 98 deletions(-)

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 06880c0f7b..078f51bb55 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -464,12 +464,14 @@ typedef struct BrinOpcInfo
 
    <varlistentry>
     <term><function>bool consistent(BrinDesc *bdesc, BrinValues *column,
-       ScanKey key)</function></term>
+       ScanKey *keys, int nkeys)</function></term>
     <listitem>
      <para>
-      Returns whether the ScanKey is consistent with the given indexed
-      values for a range.
+      Returns whether all the ScanKey entries are consistent with the given
+      indexed values for a range.
       The attribute number to use is passed as part of the scan key.
+      Multiple scan keys for the same attribute may be passed at once, the
+      number of entries is determined by the <literal>nkeys</literal> parameter.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 27ba596c6e..33f4e2c15c 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -390,6 +390,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey   **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -411,6 +414,65 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure. We don't know which attributes have scan
+	 * keys, so we allocate space for all attributes. That may use more memory
+	 * but it's probably cheaper than determining which attributes are used.
+	 *
+	 * XXX The widest index can have 32 attributes, so the amount of wasted
+	 * memory is negligible. We could invent a more compact approach (with
+	 * just space for used attributes) but that would make the matching more
+	 * complex so it's not a good trade-off.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/* Preprocess the scan keys - split them into per-attribute arrays. */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation used in the
+		 * index column (but only if the search is not IS NULL/ IS NOT NULL).
+		 * Otherwise we shouldn't be using this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this attribute, so init the array of keys. */
+		if (!keys[keyattno - 1])
+		{
+			FmgrInfo   *tmp;
+
+			/*
+			 * This is a bit of an overkill - we don't know how many scan keys
+			 * are there for this attribute, so we simply allocate the largest
+			 * number possible (as if all keys were for this attribute). This
+			 * may waste a bit of memory, but we only expect small number of
+			 * scan keys in general, so this should be negligible, and
+			 * repeated repalloc calls are not free either.
+			 */
+			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -471,7 +533,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int			attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -481,50 +543,38 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
+					Oid			collation;
 
-					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
-					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
-
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
-
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					/* skip attributes without any scan keys */
+					if (nkeys[attno - 1] == 0)
+						continue;
+
+					bval = &dtup->bt_columns[attno - 1];
+
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
 					 *
-					 * When there are multiple scan keys, failure to meet the
-					 * criteria for a single one of them is enough to discard
-					 * the range as a whole, so break out of the loop as soon
-					 * as a false return value is obtained.
+					 * XXX We simply use the collation from the first key (it
+					 * has to be the same for all keys for the same attribue).
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
-											key->sk_collation,
+					collation = keys[attno - 1][0]->sk_collation;
+
+					/* Check all keys at once */
+					add = FunctionCall4Coll(&consistentFn[attno - 1],
+											collation,
 											PointerGetDatum(bdesc),
 											PointerGetDatum(bval),
-											PointerGetDatum(key));
+											PointerGetDatum(keys[attno - 1]),
+											Int32GetDatum(nkeys[attno - 1]));
 					addrange = DatumGetBool(add);
 					if (!addrange)
 						break;
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 12e5bddd1f..cf2d029048 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -85,6 +85,8 @@ static FmgrInfo *inclusion_get_procinfo(BrinDesc *bdesc, uint16 attno,
 										uint16 procnum);
 static FmgrInfo *inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 												 Oid subtype, uint16 strategynum);
+static bool inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column,
+									 ScanKey key, Oid colloid);
 
 
 /*
@@ -251,6 +253,10 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 /*
  * BRIN inclusion consistent function
  *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches the IS NULL keys, we check the regular scan keys.
+ *
  * All of the strategies are optional.
  */
 Datum
@@ -258,24 +264,31 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	Datum		unionval;
-	AttrNumber	attno;
-	Datum		query;
-	FmgrInfo   *finfo;
-	Datum		result;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/* Handle IS NULL/IS NOT NULL tests */
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -284,7 +297,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -293,7 +311,14 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* If it is all nulls, it cannot possibly be consistent. */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
@@ -301,10 +326,45 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
 		PG_RETURN_BOOL(true);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
+	/* Check that the range is consistent with all regular scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!inclusion_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_BOOL(false);
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * inclusion_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+inclusion_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+						 Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		query = key->sk_argument;
+	Datum		unionval = column->bv_values[INCLUSION_UNION];
+	Datum		result;
+
+	/* This should be called only for regular keys, not for IS [NOT] NULL. */
+	Assert(!(key->sk_flags & SK_ISNULL));
+
 	switch (key->sk_strategy)
 	{
 			/*
@@ -324,49 +384,49 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverLeftStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTRightStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverBelowStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAboveStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTOverAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		case RTAboveStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTOverBelowStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 			/*
 			 * Overlap and contains strategies
@@ -384,7 +444,7 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													key->sk_strategy);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Contained by strategies
@@ -404,9 +464,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 			/*
 			 * Adjacent strategy
@@ -423,12 +483,12 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTOverlapStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTAdjacentStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+			return DatumGetBool(result);
 
 			/*
 			 * Basic comparison strategies
@@ -458,9 +518,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTRightStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTSameStrategyNumber:
 		case RTEqualStrategyNumber:
@@ -468,30 +528,30 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 													RTContainsStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterEqualStrategyNumber:
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
 			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+				return true;
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+			return DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
 
 		case RTGreaterStrategyNumber:
 			/* no need to check for empty elements */
 			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
 													RTLeftStrategyNumber);
 			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+			return !DatumGetBool(result);
 
 		default:
 			/* shouldn't happen */
 			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+			return false;
 	}
 }
 
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 2ffbd9bf0d..e116084a02 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -30,6 +30,8 @@ typedef struct MinmaxOpaque
 
 static FmgrInfo *minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
 											  Oid subtype, uint16 strategynum);
+static bool minmax_consistent_key(BrinDesc *bdesc, BrinValues *column,
+								  ScanKey key, Oid colloid);
 
 
 Datum
@@ -140,29 +142,41 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's min/max
  * values.  Return true if so, false otherwise.
+ *
+ * We inspect the IS NULL scan keys first, which allows us to make a decision
+ * without looking at the contents of the page range. Only when the page range
+ * matches all those keys, we check the regular scan keys.
  */
 Datum
 brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
-	Oid			colloid = PG_GET_COLLATION(),
-				subtype;
-	AttrNumber	attno;
-	Datum		value;
-	Datum		matches;
-	FmgrInfo   *finfo;
-
-	Assert(key->sk_attno == column->bv_attno);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	int			keyno;
+	bool		has_regular_keys = false;
 
 	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
+		ScanKey		key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* Skip regular scan keys (and remember that we have some). */
+		if ((!key->sk_flags & SK_ISNULL))
+		{
+			has_regular_keys = true;
+			continue;
+		}
+
 		if (key->sk_flags & SK_SEARCHNULL)
 		{
 			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+				continue;		/* this key is fine, continue */
+
 			PG_RETURN_BOOL(false);
 		}
 
@@ -171,7 +185,12 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		 * only nulls.
 		 */
 		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
+		{
+			if (column->bv_allnulls)
+				PG_RETURN_BOOL(false);
+
+			continue;
+		}
 
 		/*
 		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
@@ -180,13 +199,52 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(false);
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!has_regular_keys)
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If is all nulls, it cannot possibly be consistent (at this point we
+	 * know there are at least some regular scan keys).
+	 */
 	if (column->bv_allnulls)
 		PG_RETURN_BOOL(false);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
+	/* Check that the range is consistent with all scan keys. */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
+
+		/*
+		 * When there are multiple scan keys, failure to meet the criteria for
+		 * a single one of them is enough to discard the range as a whole, so
+		 * break out of the loop as soon as a false return value is obtained.
+		 */
+		if (!minmax_consistent_key(bdesc, column, key, colloid))
+			PG_RETURN_DATUM(false);;
+	}
+
+	PG_RETURN_DATUM(true);
+}
+
+/*
+ * minmax_consistent_key
+ *		Determine if the range is consistent with a single scan key.
+ */
+static bool
+minmax_consistent_key(BrinDesc *bdesc, BrinValues *column, ScanKey key,
+					  Oid colloid)
+{
+	FmgrInfo   *finfo;
+	AttrNumber	attno = key->sk_attno;
+	Oid			subtype = key->sk_subtype;
+	Datum		value = key->sk_argument;
+	Datum		matches;
+
 	switch (key->sk_strategy)
 	{
 		case BTLessStrategyNumber:
@@ -229,7 +287,7 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	PG_RETURN_DATUM(matches);
+	return DatumGetBool(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 6d4253c05e..2c4f9a3eff 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -97,8 +97,8 @@ brinvalidate(Oid opclassoid)
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
-											3, 3, INTERNALOID, INTERNALOID,
-											INTERNALOID);
+											4, 4, INTERNALOID, INTERNALOID,
+											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index c831b55bf9..5c9ede2578 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202103191
+#define CATALOG_VERSION_NO	202103221
 
 #endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e259531f60..b9f4afba05 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8215,7 +8215,7 @@
   prosrc => 'brin_minmax_add_value' },
 { oid => '3385', descr => 'BRIN minmax support',
   proname => 'brin_minmax_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_minmax_consistent' },
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
@@ -8231,7 +8231,7 @@
   prosrc => 'brin_inclusion_add_value' },
 { oid => '4107', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
+  proargtypes => 'internal internal internal int4',
   prosrc => 'brin_inclusion_consistent' },
 { oid => '4108', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_union', prorettype => 'bool',
-- 
2.30.2

0003-Move-IS-NOT-NULL-handling-from-BRIN-support-20210322.patchtext/x-patch; charset=UTF-8; name=0003-Move-IS-NOT-NULL-handling-from-BRIN-support-20210322.patchDownload
From a6602db20e3e9970eefbadff325d7769f310fe88 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Mon, 22 Mar 2021 00:24:29 +0100
Subject: [PATCH 3/8] Move IS [NOT] NULL handling from BRIN support functions

The handling of IS [NOT] NULL clauses is independent of an opclass, and
most of the code was exactly the same in both minmax and inclusion. So
instead move the code from support procedures to the AM methods etc.

This simplifies the code quite a bit - especially the support procedures
quite a bit, as they don't need to care about NULL values and flags at
all. It also means the IS [NOT] NULL clauses can be evaluated without
invoking the support procedure at all.

Author: Tomas Vondra <tomas.vondra@postgresql.org>
Author: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Nikita Glukhov <n.gluhov@postgrespro.ru>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@enterprisedb.com>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c           | 293 +++++++++++++++++------
 src/backend/access/brin/brin_inclusion.c |  96 +-------
 src/backend/access/brin/brin_minmax.c    |  93 +------
 src/include/access/brin_internal.h       |   3 +
 4 files changed, 244 insertions(+), 241 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 33f4e2c15c..efda84aba3 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -35,6 +35,7 @@
 #include "storage/freespace.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -77,7 +78,9 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
 static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
-
+static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
+								BrinMemTuple *dtup, Datum *values, bool *nulls);
+static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
 
 /*
  * BRIN handler function: return IndexAmRoutine with access method parameters
@@ -179,7 +182,6 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 		OffsetNumber off;
 		BrinTuple  *brtup;
 		BrinMemTuple *dtup;
-		int			keyno;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -243,31 +245,7 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 		dtup = brin_deform_tuple(bdesc, brtup, NULL);
 
-		/*
-		 * Compare the key values of the new tuple to the stored index values;
-		 * our deformed tuple will get updated if the new tuple doesn't fit
-		 * the original range (note this means we can't break out of the loop
-		 * early). Make a note of whether this happens, so that we know to
-		 * insert the modified tuple later.
-		 */
-		for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
-		{
-			Datum		result;
-			BrinValues *bval;
-			FmgrInfo   *addValue;
-
-			bval = &dtup->bt_columns[keyno];
-			addValue = index_getprocinfo(idxRel, keyno + 1,
-										 BRIN_PROCNUM_ADDVALUE);
-			result = FunctionCall4Coll(addValue,
-									   idxRel->rd_indcollation[keyno],
-									   PointerGetDatum(bdesc),
-									   PointerGetDatum(bval),
-									   values[keyno],
-									   nulls[keyno]);
-			/* if that returned true, we need to insert the updated tuple */
-			need_insert |= DatumGetBool(result);
-		}
+		need_insert = add_values_to_range(idxRel, bdesc, dtup, values, nulls);
 
 		if (!need_insert)
 		{
@@ -390,8 +368,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey   **keys;
-	int		   *nkeys;
+	ScanKey   **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -420,13 +400,18 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * keys, so we allocate space for all attributes. That may use more memory
 	 * but it's probably cheaper than determining which attributes are used.
 	 *
+	 * We keep null and regular keys separate, so that we can pass just the
+	 * regular keys to the consistent function easily.
+	 *
 	 * XXX The widest index can have 32 attributes, so the amount of wasted
 	 * memory is negligible. We could invent a more compact approach (with
 	 * just space for used attributes) but that would make the matching more
 	 * complex so it's not a good trade-off.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/* Preprocess the scan keys - split them into per-attribute arrays. */
 	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
@@ -444,23 +429,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				TupleDescAttr(bdesc->bd_tupdesc,
 							  keyattno - 1)->attcollation));
 
-		/* First time we see this attribute, so init the array of keys. */
-		if (!keys[keyattno - 1])
+		/*
+		 * First time we see this index attribute, so init as needed.
+		 *
+		 * This is a bit of an overkill - we don't know how many scan keys are
+		 * there for this attribute, so we simply allocate the largest number
+		 * possible (as if all keys were for this attribute). This may waste a
+		 * bit of memory, but we only expect small number of scan keys in
+		 * general, so this should be negligible, and repeated repalloc calls
+		 * are not free either.
+		 */
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			/*
-			 * This is a bit of an overkill - we don't know how many scan keys
-			 * are there for this attribute, so we simply allocate the largest
-			 * number possible (as if all keys were for this attribute). This
-			 * may waste a bit of memory, but we only expect small number of
-			 * scan keys in general, so this should be negligible, and
-			 * repeated repalloc calls are not free either.
-			 */
-			keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			/* No key/null arrays for this attribute. */
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -468,9 +453,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -549,15 +548,58 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					Datum		add;
 					Oid			collation;
 
-					/* skip attributes without any scan keys */
-					if (nkeys[attno - 1] == 0)
+					/*
+					 * skip attributes without any scan keys (both regular and
+					 * IS [NOT] NULL)
+					 */
+					if (nkeys[attno - 1] == 0 && nnullkeys[attno - 1] == 0)
 						continue;
 
 					bval = &dtup->bt_columns[attno - 1];
 
+					/*
+					 * First check if there are any IS [NOT] NULL scan keys,
+					 * and if we're violating them. In that case we can
+					 * terminate early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine
+					 * mismatch within this loop.
+					 */
+					if (bdesc->bd_info[attno - 1]->oi_regular_nulls &&
+						!check_null_keys(bval, nullkeys[attno - 1],
+										 nnullkeys[attno - 1]))
+					{
+						/*
+						 * If any of the IS [NOT] NULL keys failed, the page
+						 * range as a whole can't pass. So terminate the loop.
+						 */
+						addrange = false;
+						break;
+					}
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all
+					 * passed. If there are no regular scan keys, we're done -
+					 * the page range matches. If there are regular keys, but
+					 * the page range is marked as 'all nulls' it can't
+					 * possibly pass (we're assuming the operators are
+					 * strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
 					Assert((nkeys[attno - 1] > 0) &&
 						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -663,7 +705,6 @@ brinbuildCallback(Relation index,
 {
 	BrinBuildState *state = (BrinBuildState *) brstate;
 	BlockNumber thisblock;
-	int			i;
 
 	thisblock = ItemPointerGetBlockNumber(tid);
 
@@ -692,25 +733,8 @@ brinbuildCallback(Relation index,
 	}
 
 	/* Accumulate the current tuple into the running state */
-	for (i = 0; i < state->bs_bdesc->bd_tupdesc->natts; i++)
-	{
-		FmgrInfo   *addValue;
-		BrinValues *col;
-		Form_pg_attribute attr = TupleDescAttr(state->bs_bdesc->bd_tupdesc, i);
-
-		col = &state->bs_dtuple->bt_columns[i];
-		addValue = index_getprocinfo(index, i + 1,
-									 BRIN_PROCNUM_ADDVALUE);
-
-		/*
-		 * Update dtuple state, if and as necessary.
-		 */
-		FunctionCall4Coll(addValue,
-						  attr->attcollation,
-						  PointerGetDatum(state->bs_bdesc),
-						  PointerGetDatum(col),
-						  values[i], isnull[i]);
-	}
+	(void) add_values_to_range(index, state->bs_bdesc, state->bs_dtuple,
+							   values, isnull);
 }
 
 /*
@@ -1489,6 +1513,39 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
 		FmgrInfo   *unionFn;
 		BrinValues *col_a = &a->bt_columns[keyno];
 		BrinValues *col_b = &db->bt_columns[keyno];
+		BrinOpcInfo *opcinfo = bdesc->bd_info[keyno];
+
+		if (opcinfo->oi_regular_nulls)
+		{
+			/* Adjust "hasnulls". */
+			if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+				col_a->bv_hasnulls = true;
+
+			/* If there are no values in B, there's nothing left to do. */
+			if (col_b->bv_allnulls)
+				continue;
+
+			/*
+			 * Adjust "allnulls".  If A doesn't have values, just copy the
+			 * values from B into A, and we're done.  We cannot run the
+			 * operators in this case, because values in A might contain
+			 * garbage.  Note we already established that B contains values.
+			 */
+			if (col_a->bv_allnulls)
+			{
+				int			i;
+
+				col_a->bv_allnulls = false;
+
+				for (i = 0; i < opcinfo->oi_nstored; i++)
+					col_a->bv_values[i] =
+						datumCopy(col_b->bv_values[i],
+								  opcinfo->oi_typcache[i]->typbyval,
+								  opcinfo->oi_typcache[i]->typlen);
+
+				continue;
+			}
+		}
 
 		unionFn = index_getprocinfo(bdesc->bd_index, keyno + 1,
 									BRIN_PROCNUM_UNION);
@@ -1542,3 +1599,103 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	 */
 	FreeSpaceMapVacuum(idxrel);
 }
+
+static bool
+add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup,
+					Datum *values, bool *nulls)
+{
+	int			keyno;
+	bool		modified = false;
+
+	/*
+	 * Compare the key values of the new tuple to the stored index values; our
+	 * deformed tuple will get updated if the new tuple doesn't fit the
+	 * original range (note this means we can't break out of the loop early).
+	 * Make a note of whether this happens, so that we know to insert the
+	 * modified tuple later.
+	 */
+	for (keyno = 0; keyno < bdesc->bd_tupdesc->natts; keyno++)
+	{
+		Datum		result;
+		BrinValues *bval;
+		FmgrInfo   *addValue;
+
+		bval = &dtup->bt_columns[keyno];
+
+		if (bdesc->bd_info[keyno]->oi_regular_nulls && nulls[keyno])
+		{
+			/*
+			 * If the new value is null, we record that we saw it if it's the
+			 * first one; otherwise, there's nothing to do.
+			 */
+			if (!bval->bv_hasnulls)
+			{
+				bval->bv_hasnulls = true;
+				modified = true;
+			}
+
+			continue;
+		}
+
+		addValue = index_getprocinfo(idxRel, keyno + 1,
+									 BRIN_PROCNUM_ADDVALUE);
+		result = FunctionCall4Coll(addValue,
+								   idxRel->rd_indcollation[keyno],
+								   PointerGetDatum(bdesc),
+								   PointerGetDatum(bval),
+								   values[keyno],
+								   nulls[keyno]);
+		/* if that returned true, we need to insert the updated tuple */
+		modified |= DatumGetBool(result);
+	}
+
+	return modified;
+}
+
+static bool
+check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys)
+{
+	int			keyno;
+
+	/*
+	 * First check if there are any IS [NOT] NULL scan keys, and if we're
+	 * violating them.
+	 */
+	for (keyno = 0; keyno < nnullkeys; keyno++)
+	{
+		ScanKey		key = nullkeys[keyno];
+
+		Assert(key->sk_attno == bval->bv_attno);
+
+		/* Handle only IS NULL/IS NOT NULL tests */
+		if (!(key->sk_flags & SK_ISNULL))
+			continue;
+
+		if (key->sk_flags & SK_SEARCHNULL)
+		{
+			/* IS NULL scan key, but range has no NULLs */
+			if (!bval->bv_allnulls && !bval->bv_hasnulls)
+				return false;
+		}
+		else if (key->sk_flags & SK_SEARCHNOTNULL)
+		{
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (bval->bv_allnulls)
+				return false;
+		}
+		else
+		{
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and thus return false with NULL value in
+			 * the scan key.
+			 */
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index cf2d029048..d8cfc9d909 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -110,6 +110,7 @@ brin_inclusion_opcinfo(PG_FUNCTION_ARGS)
 	 */
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(3)) + sizeof(InclusionOpaque));
 	result->oi_nstored = 3;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (InclusionOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(3));
 
@@ -141,7 +142,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_BOOL(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *finfo;
 	Datum		result;
@@ -149,18 +150,7 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	AttrNumber	attno;
 	Form_pg_attribute attr;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -268,52 +258,9 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* Handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
 
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
-
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -331,9 +278,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* Skip IS NULL/IS NOT NULL keys (already handled above). */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -574,37 +520,11 @@ brin_inclusion_union(PG_FUNCTION_ARGS)
 	Datum		result;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls". */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do. */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[INCLUSION_UNION] =
-			datumCopy(col_b->bv_values[INCLUSION_UNION],
-					  attr->attbyval, attr->attlen);
-		col_a->bv_values[INCLUSION_UNMERGEABLE] =
-			col_b->bv_values[INCLUSION_UNMERGEABLE];
-		col_a->bv_values[INCLUSION_CONTAINS_EMPTY] =
-			col_b->bv_values[INCLUSION_CONTAINS_EMPTY];
-		PG_RETURN_VOID();
-	}
-
 	/* If B includes empty elements, mark A similarly, if needed. */
 	if (!DatumGetBool(col_a->bv_values[INCLUSION_CONTAINS_EMPTY]) &&
 		DatumGetBool(col_b->bv_values[INCLUSION_CONTAINS_EMPTY]))
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index e116084a02..330bed0487 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -48,6 +48,7 @@ brin_minmax_opcinfo(PG_FUNCTION_ARGS)
 	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(2)) +
 					 sizeof(MinmaxOpaque));
 	result->oi_nstored = 2;
+	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(2));
 	result->oi_typcache[0] = result->oi_typcache[1] =
@@ -69,7 +70,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
 	Datum		newval = PG_GETARG_DATUM(2);
-	bool		isnull = PG_GETARG_DATUM(3);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
 	Oid			colloid = PG_GET_COLLATION();
 	FmgrInfo   *cmpFn;
 	Datum		compar;
@@ -77,18 +78,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 
-	/*
-	 * If the new value is null, we record that we saw it if it's the first
-	 * one; otherwise, there's nothing to do.
-	 */
-	if (isnull)
-	{
-		if (column->bv_hasnulls)
-			PG_RETURN_BOOL(false);
-
-		column->bv_hasnulls = true;
-		PG_RETURN_BOOL(true);
-	}
+	Assert(!isnull);
 
 	attno = column->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
@@ -156,52 +146,9 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION();
 	int			keyno;
-	bool		has_regular_keys = false;
-
-	/* handle IS NULL/IS NOT NULL tests */
-	for (keyno = 0; keyno < nkeys; keyno++)
-	{
-		ScanKey		key = keys[keyno];
-
-		Assert(key->sk_attno == column->bv_attno);
-
-		/* Skip regular scan keys (and remember that we have some). */
-		if ((!key->sk_flags & SK_ISNULL))
-		{
-			has_regular_keys = true;
-			continue;
-		}
 
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				continue;		/* this key is fine, continue */
-
-			PG_RETURN_BOOL(false);
-		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-		{
-			if (column->bv_allnulls)
-				PG_RETURN_BOOL(false);
-
-			continue;
-		}
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!has_regular_keys)
-		PG_RETURN_BOOL(true);
+	/* make sure we got some scan keys */
+	Assert((nkeys > 0) && (keys != NULL));
 
 	/*
 	 * If is all nulls, it cannot possibly be consistent (at this point we
@@ -215,9 +162,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey		key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		/*
 		 * When there are multiple scan keys, failure to meet the criteria for
@@ -307,34 +253,11 @@ brin_minmax_union(PG_FUNCTION_ARGS)
 	bool		needsadj;
 
 	Assert(col_a->bv_attno == col_b->bv_attno);
-
-	/* Adjust "hasnulls" */
-	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
-		col_a->bv_hasnulls = true;
-
-	/* If there are no values in B, there's nothing left to do */
-	if (col_b->bv_allnulls)
-		PG_RETURN_VOID();
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
 
 	attno = col_a->bv_attno;
 	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
 
-	/*
-	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
-	 * B into A, and we're done.  We cannot run the operators in this case,
-	 * because values in A might contain garbage.  Note we already established
-	 * that B contains values.
-	 */
-	if (col_a->bv_allnulls)
-	{
-		col_a->bv_allnulls = false;
-		col_a->bv_values[0] = datumCopy(col_b->bv_values[0],
-										attr->attbyval, attr->attlen);
-		col_a->bv_values[1] = datumCopy(col_b->bv_values[1],
-										attr->attbyval, attr->attlen);
-		PG_RETURN_VOID();
-	}
-
 	/* Adjust minimum, if B's min is less than A's min */
 	finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
 										 BTLessStrategyNumber);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index 78c89a6961..79440ebe7b 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -27,6 +27,9 @@ typedef struct BrinOpcInfo
 	/* Number of columns stored in an index column of this opclass */
 	uint16		oi_nstored;
 
+	/* Regular processing of NULLs in BrinValues? */
+	bool		oi_regular_nulls;
+
 	/* Opaque pointer for the opclass' private use */
 	void	   *oi_opaque;
 
-- 
2.30.2

0004-Optimize-allocations-in-bringetbitmap-20210322.patchtext/x-patch; charset=UTF-8; name=0004-Optimize-allocations-in-bringetbitmap-20210322.patchDownload
From 66384f643e985fe54e0d0712cfcf17f5bb3251ca Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Mon, 22 Mar 2021 00:24:33 +0100
Subject: [PATCH 4/8] Optimize allocations in bringetbitmap

The bringetbitmap function allocates memory for various purposes, which
may be quite expensive, depending on the number of scan keys. Instead of
allocating them separately, allocate one bit chunk of memory an carve it
into smaller pieces as needed - all the pieces have the same lifespan,
and it saves quite a bit of CPU and memory overhead.

Author: Tomas Vondra <tomas.vondra@postgresql.org>
Reviewed-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: Mark Dilger <hornschnorter@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Masahiko Sawada <masahiko.sawada@enterprisedb.com>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
---
 src/backend/access/brin/brin.c | 60 ++++++++++++++++++++++++++--------
 1 file changed, 47 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index efda84aba3..0bf25fd05b 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -373,6 +373,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	int		   *nkeys,
 			   *nnullkeys;
 	int			keyno;
+	char	   *ptr;
+	Size		len;
+	char	   *tmp PG_USED_FOR_ASSERTS_ONLY;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -403,15 +406,52 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * We keep null and regular keys separate, so that we can pass just the
 	 * regular keys to the consistent function easily.
 	 *
+	 * To reduce the allocation overhead, we allocate one big chunk and then
+	 * carve it into smaller arrays ourselves. All the pieces have exactly the
+	 * same lifetime, so that's OK.
+	 *
 	 * XXX The widest index can have 32 attributes, so the amount of wasted
 	 * memory is negligible. We could invent a more compact approach (with
 	 * just space for used attributes) but that would make the matching more
 	 * complex so it's not a good trade-off.
 	 */
-	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
-	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
-	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	len =
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* regular keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts) +
+		MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts) +	/* NULL keys */
+		MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys) * bdesc->bd_tupdesc->natts +
+		MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	ptr = palloc(len);
+	tmp = ptr;
+
+	keys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nullkeys = (ScanKey **) ptr;
+	ptr += MAXALIGN(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+
+	nkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	nnullkeys = (int *) ptr;
+	ptr += MAXALIGN(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	for (int i = 0; i < bdesc->bd_tupdesc->natts; i++)
+	{
+		keys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+
+		nullkeys[i] = (ScanKey *) ptr;
+		ptr += MAXALIGN(sizeof(ScanKey) * scan->numberOfKeys);
+	}
+
+	Assert(tmp + len == ptr);
+
+	/* zero the number of keys */
+	memset(nkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
+	memset(nnullkeys, 0, sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/* Preprocess the scan keys - split them into per-attribute arrays. */
 	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
@@ -443,9 +483,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		{
 			FmgrInfo   *tmp;
 
-			/* No key/null arrays for this attribute. */
-			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
-			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
+			/* First time we see this attribute, so no key/null keys. */
+			Assert(nkeys[keyattno - 1] == 0);
+			Assert(nnullkeys[keyattno - 1] == 0);
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -456,17 +496,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		/* Add key to the proper per-attribute array. */
 		if (key->sk_flags & SK_ISNULL)
 		{
-			if (!nullkeys[keyattno - 1])
-				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
 			nnullkeys[keyattno - 1]++;
 		}
 		else
 		{
-			if (!keys[keyattno - 1])
-				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
 			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
 			nkeys[keyattno - 1]++;
 		}
-- 
2.30.2

0005-BRIN-bloom-indexes-20210322.patchtext/x-patch; charset=UTF-8; name=0005-BRIN-bloom-indexes-20210322.patchDownload
From 390c4a1867228a0a6b324843242c46ce7b24ed84 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Mon, 22 Mar 2021 00:24:37 +0100
Subject: [PATCH 5/8] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired false
positive rate.

The opclasses do not operate directly on the indexed values, but on
32-bit hashes of the values. That assumes the hash functions for data
types have a low number of collisions, good performance etc. Collisions
should not be a huge issue though, because the number of values in a
BRIN ranges is usually fairly small.

Bump catversion, due to various catalog changes.

Author: Tomas Vondra <tomas.vondra@postgresql.org>
Reviewed-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Reviewed-by: Nico Williams <nico@cryptonector.com>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 226 +++++-
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 809 ++++++++++++++++++++++
 src/include/catalog/catversion.h          |   2 +-
 src/include/catalog/pg_amop.dat           | 116 ++++
 src/include/catalog/pg_amproc.dat         | 447 ++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 +
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +-
 src/test/regress/expected/brin_bloom.out  | 428 ++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 376 ++++++++++
 17 files changed, 2567 insertions(+), 8 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 078f51bb55..cb4f9b08b9 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -115,7 +115,8 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
   operator classes store the minimum and the maximum values appearing
   in the indexed column within the range.  The <firstterm>inclusion</firstterm>
   operator classes store a value which includes the values in the indexed
-  column within the range.
+  column within the range.  The <firstterm>bloom</firstterm> operator
+  classes build a Bloom filter for all values in the range.
  </para>
 
  <table id="brin-builtin-opclasses-table">
@@ -154,6 +155,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +169,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +183,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +197,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +211,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +225,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +249,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +263,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +277,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +291,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +305,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +319,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +333,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +347,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +361,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +375,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +389,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +421,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +435,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +449,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +463,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +477,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +491,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +505,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -409,6 +530,55 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    </tbody>
   </tgroup>
  </table>
+
+  <sect2 id="brin-builtin-opclasses--parameters">
+   <title>Operator Class Parameters</title>
+
+   <para>
+    Some of the built-in operator classes allow specifying parameters affecting
+    behavior of the operator class.  Each operator class has its own set of
+    allowed parameters.  Only the <literal>bloom</literal> operator class
+    allows specifying parameters:
+   </para>
+
+   <para>
+    <acronym>bloom</acronym> operator classes accept these parameters:
+   </para>
+
+   <variablelist>
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default value is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be
+     between 0.0001 and 0.25. The default value is 0.01, which is 1% false
+     positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   </variablelist>
+  </sect2>
+
 </sect1>
 
 <sect1 id="brin-extensibility">
@@ -781,6 +951,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..2214fb4d0c
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,809 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a small
+ * bloom filter, we can easily (and cheaply) test whether it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * Bloom indexes are however much smaller, and support only bitmap scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * hashing scheme
+ * --------------
+ *
+ * Bloom filters require a number of independent hash functions. There are
+ * different schemes how to construct them - for example we might use
+ * hash_uint32_extended with random seeds, but that seems fairly expensive.
+ * We use a scheme requiring only two functions described in this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions h1 and h2 are calculated using hard-coded seeds,
+ * and then combined using (h1 + i * h2) to generate the hash functions.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random (when "full" about half the bits is set
+ * to 1, randomly), compression can't help very much.
+ *
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This should reduce the number of distinct values
+ * in the page range, making the filter smaller (with fixed false positive
+ * rate). Even for random data sets this should help, as the number of rows
+ * per heap page is limited (to ~290 with very narrow tables, likely ~20
+ * in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distribute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_page.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions. We only have one, which is
+ * used to calculate hash of the input value.
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001	/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25	/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * And estimate of the largest bloom we can fit onto a page. This is not
+ * a perfect guarantee, for a couple of reasons. For example, the row may
+ * be larger because the index has multiple columns.
+ */
+#define BloomMaxFilterSize \
+	MAXALIGN_DOWN(BLCKSZ - \
+				  (MAXALIGN(SizeOfPageHeaderData + \
+							sizeof(ItemIdData)) + \
+				   MAXALIGN(sizeof(BrinSpecialSpace)) + \
+				   SizeOfBrinTuple))
+
+/*
+ * Seeds used to calculate two hash functions h1 and h2, which are then used
+ * to generate k hashes using the (h1 + i * h2) scheme.
+ */
+#define BLOOM_SEED_1	0x71d924af
+#define BLOOM_SEED_2	0xba48b314
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes that
+ * are not entirely 0. But while indexes don't support TOAST, the varlena can
+ * still be compressed. So this seems unnecessary, because the compression
+ * should do the same job.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and then
+ * stop using it (and not store the bitmap, to save space) when the false
+ * positive rate gets too high. But even if the false positive rate exceeds the
+ * desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16		flags;
+
+	/* fields for the HASHED phase */
+	uint8		nhashes;		/* number of hash functions */
+	uint32		nbits;			/* number of bits in the bitmap (size) */
+	uint32		nbits_set;		/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+
+}			BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected values
+ * and the requested false positive rate. The filter is stored as varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size		len;
+	BloomFilter *filter;
+
+	int			nbits;			/* size of filter / number of bits */
+	int			nbytes;			/* size of filter / number of bytes */
+
+	double		k;				/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate >= BLOOM_MIN_FALSE_POSITIVE_RATE) &&
+		   (false_positive_rate < BLOOM_MAX_FALSE_POSITIVE_RATE));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	nbits = ceil(-(ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	nbytes = ((nbits + 7) / 8);
+	nbits = nbytes * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * Initially the bloom filter is just zeroes and so very compressible, but
+	 * as we add values it gets more and more random, and so less and less
+	 * compressible. So initially everything fits on the page, but we might
+	 * get surprising failures later - we want to prevent that, so we reject
+	 * bloom filter that are obviously too large.
+	 *
+	 * XXX It's not uncommon to oversize the bloom filter a bit, to defend
+	 * against unexpected data anomalies (parts of table with more distinct
+	 * values per range etc.). But we still need to make sure even the
+	 * oversized filter fits on page, if such need arises.
+	 *
+	 * XXX This check is not perfect, because the index may have multiple
+	 * filters that are small individually, but too large when combined.
+	 */
+	if (nbytes > BloomMaxFilterSize)
+		elog(ERROR, "the bloom filter is too large (%d > %zu)", nbytes,
+			 BloomMaxFilterSize);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * nbits / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so the
+	 * varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + nbytes;
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = nbits;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter * filter, uint32 value, bool *updated)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = hash_bytes_uint32_extended(value, BLOOM_SEED_1) % filter->nbits;
+	h2 = hash_bytes_uint32_extended(value, BLOOM_SEED_2) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (!(filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter * filter, uint32 value)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* calculate the two hashes */
+	h1 = hash_bytes_uint32_extended(value, BLOOM_SEED_1) % filter->nbits;
+	h2 = hash_bytes_uint32_extended(value, BLOOM_SEED_2) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (!(filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash), but
+	 * let's keep the array just like inclusion and minman opclasses, for
+	 * consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+}			BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+									uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double		ndistinct;
+	double		maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this case to
+	 * maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a couple of
+	 * safeties to avoid using unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples, in the
+	 * range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from
+	 * BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to
+				 * return the current page range if the minimum value in the
+				 * range <= scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters for now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing text
+	 * input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 5c9ede2578..017ef60217 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202103221
+#define CATALOG_VERSION_NO	202103222
 
 #endif
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..04d678f96a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,20 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2040,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2062,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2084,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2155,14 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2180,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2202,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2224,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2266,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2288,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2439,20 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2470,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2492,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2548,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2570,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2636,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..2af8af3f4e 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashbpchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..7cc3d59a8c 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '4572',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '4573',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '4574',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '4575',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '4576',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '4577',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '4578',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '4579',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '4580',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '4581',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '4582',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '4583',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '4584',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '4585',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '4586',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '4587',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '4588',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '4589',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '4590',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b9f4afba05..c89df733f7 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8238,6 +8238,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '4591', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '4592', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '4593', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '4594', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '4595', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11411,4 +11431,18 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
+{ oid => '4596', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '4597', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '4598', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '4599', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8959c2f53b..2a82a3e544 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,5 +679,10 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-
+{ oid => '4600',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..32c56a996a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,428 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..48ce3f7411 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
+ 4600 | pg_brin_bloom_summary
  5017 | pg_mcv_list
-(4 rows)
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 70c38309d7..95927a7bae 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index d81d04136c..c02a981c78 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..5d499208e3
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,376 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.30.2

0006-BRIN-minmax-multi-indexes-20210322.patchtext/x-patch; charset=UTF-8; name=0006-BRIN-minmax-multi-indexes-20210322.patchDownload
From 65ba5c6d7c0d00506485bfcc16cae5fd9ea4766f Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Mon, 22 Mar 2021 00:24:44 +0100
Subject: [PATCH 6/8] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@postgresql.org>
Reviewed-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  280 +-
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 3036 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   23 +-
 src/include/access/brin_tuple.h             |    8 +
 src/include/access/transam.h                |   10 +-
 src/include/catalog/catversion.h            |    2 +-
 src/include/catalog/pg_amop.dat             |  544 ++++
 src/include/catalog/pg_amproc.dat           |  600 +++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   85 +
 src/include/catalog/pg_type.dat             |    6 +
 src/test/regress/expected/brin_multi.out    |  450 +++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  403 +++
 19 files changed, 5526 insertions(+), 30 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index cb4f9b08b9..3ca8236272 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -116,7 +116,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
   in the indexed column within the range.  The <firstterm>inclusion</firstterm>
   operator classes store a value which includes the values in the indexed
   column within the range.  The <firstterm>bloom</firstterm> operator
-  classes build a Bloom filter for all values in the range.
+  classes build a Bloom filter for all values in the range.  The
+  <firstterm>minmax-multi</firstterm> operator classes store multiple
+  minimum and maximum values, representing values appearing in the indexed
+  column within the range.
  </para>
 
  <table id="brin-builtin-opclasses-table">
@@ -211,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -225,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -239,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -263,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -277,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -291,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -305,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -319,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -333,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -347,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -375,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -389,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -403,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -449,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -463,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -477,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -491,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -505,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -519,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -537,8 +711,8 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    <para>
     Some of the built-in operator classes allow specifying parameters affecting
     behavior of the operator class.  Each operator class has its own set of
-    allowed parameters.  Only the <literal>bloom</literal> operator class
-    allows specifying parameters:
+    allowed parameters.  Only the <literal>bloom</literal> and <literal>minmax-multi</literal>
+    operator classes allow specifying parameters:
    </para>
 
    <para>
@@ -577,6 +751,25 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    </varlistentry>
 
    </variablelist>
+
+   <para>
+    <acronym>minmax-multi</acronym> operator classes accept these parameters:
+   </para>
+
+   <variablelist>
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     either a point, or a boundary of an interval. Values must be between
+     8 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   </variablelist>
   </sect2>
 
 </sect1>
@@ -702,13 +895,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, minmax-multi, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -1005,6 +1199,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The minmax-multi operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, minmax-multi allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the minmax-multi support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-minmax-multi-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-minmax-multi-table">
+  <title>Procedure and Support Numbers for minmax-multi Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..1ca86b7fb7
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,3036 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually make the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With minmax-multi opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 32 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 50% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ * The compactions (reducing the number of values) is fairly expensive, as
+ * it requires calling the distance functions, sorting etc. So when building
+ * the summary, we use a significantly larger buffer, and only enforce the
+ * exact limit at the very end. This improves performance, and it also helps
+ * with building better ranges (due to the greedy approach).
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values */
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Sizing the insert buffer - we use 10x the number of values specified
+ * in the reloption, but we cap it to 8192 not to get too large. When
+ * the buffer gets full, we reduce the number of values by half.
+ */
+#define		MINMAX_BUFFER_FACTOR			10
+#define		MINMAX_BUFFER_MIN				256
+#define		MINMAX_BUFFER_MAX				8192
+#define		MINMAX_BUFFER_LOAD_FACTOR		0.5
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+}			MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxMultiOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange; /* number of values per range */
+} MinMaxMultiOptions;
+
+#define MINMAX_MULTI_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxMultiGetValuesPerRange(opts) \
+		((opts) && (((MinMaxMultiOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxMultiOptions *) (opts))->valuesPerRange : \
+		 MINMAX_MULTI_DEFAULT_VALUES_PER_PAGE)
+
+#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0))
+
+/*
+ * The summary of minmax-multi indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* Cache information that we need quite often. */
+	Oid			typid;
+	Oid			colloid;
+	AttrNumber	attno;
+	FmgrInfo   *cmp;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nsorted;		/* number of sorted values (ranges + points) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/*
+	 * We simply add the values into a large buffer, without any expensive
+	 * steps (sorting, deduplication, ...). The buffer is a multiple of the
+	 * target number of values, so the compaction happen less often,
+	 * amortizing the costs. We keep the actual target and compact to the
+	 * requested number of values at the very end, before serializing to
+	 * on-disk representation.
+	 */
+	/* requested number of values */
+	int			target_maxvalues;
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum		values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, with a simple header
+ * with basic metadata, followed by the boundary values. It has a varlena
+ * header, so can be treated as varlena directly.
+ *
+ * See range_serialize/range_deserialize for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* type of values stored in the data array */
+	Oid			typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(int maxvalues, SerializedRanges *range);
+
+
+/*
+ * Used to represent ranges expanded to make merging and combining easier.
+ *
+ * Each expanded range is essentially an interval, represented by min/max
+ * values, along with a flag whether it's a collapsed range (in which case
+ * the min and max values are equal). We have the flag to handle by-ref
+ * data types - we can't simply compare the datums, and this saves some
+ * calls to the type-specific comparator function.
+ */
+typedef struct ExpandedRange
+{
+	Datum		minval;			/* lower boundary */
+	Datum		maxval;			/* upper boundary */
+	bool		collapsed;		/* true if minval==maxval */
+} ExpandedRange;
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of extended ranges).
+ */
+typedef struct DistanceValue
+{
+	int			index;
+	double		value;
+} DistanceValue;
+
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+										   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+													uint16 attno, Oid subtype,
+													uint16 strategynum);
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+}			compare_context;
+
+static int	compare_values(const void *a, const void *b, void *arg);
+
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+	int			i;
+	Datum		lt;
+
+	for (i = 0; i < (nvalues - 1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i + 1]);
+		Assert(DatumGetBool(lt));
+	}
+}
+#endif
+
+/*
+ * Comprehensive check of the Ranges structure.
+ */
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+
+	/* some basic sanity checks */
+	Assert(ranges->nranges >= 0);
+	Assert(ranges->nsorted >= 0);
+	Assert(ranges->nvalues >= ranges->nsorted);
+	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
+	Assert(ranges->typid != InvalidOid);
+
+	/*
+	 * First the ranges - there are 2*nranges boundary values, and the values
+	 * have to be strictly ordered (equal values would mean the range is
+	 * collapsed, and should be stored as a point). This also guarantees that
+	 * the ranges do not overlap.
+	 */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2 * ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2 * ranges->nranges],
+					 ranges->nsorted);
+
+	/*
+	 * Check that none of the values are not covered by ranges (both sorted
+	 * and unsorted)
+	 */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		compar;
+		int			start,
+					end;
+		Datum		minvalue,
+					maxvalue;
+
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nranges == 0)
+			break;
+
+		minvalue = ranges->values[0];
+		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+		/* smaller than the smallest value in the first range */
+		if (DatumGetBool(compar))
+			continue;
+
+		/*
+		 * Is the value greater than the maxval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+		/* larger than the largest value in the last range */
+		if (DatumGetBool(compar))
+			continue;
+
+		start = 0;				/* first range */
+		end = ranges->nranges - 1;	/* last range */
+		while (true)
+		{
+			int			midpoint = (start + end) / 2;
+
+			/* this means we ran out of ranges in the last step */
+			if (start > end)
+				break;
+
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2 * midpoint];
+			maxvalue = ranges->values[2 * midpoint + 1];
+
+			/*
+			 * Is the value smaller than the minval? If yes, we'll recurse to
+			 * the left side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+			{
+				end = (midpoint - 1);
+				continue;
+			}
+
+			/*
+			 * Is the value greater than the minval? If yes, we'll recurse to
+			 * the right side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+			{
+				start = (midpoint + 1);
+				continue;
+			}
+
+			/* hey, we found a matching range */
+			Assert(false);
+		}
+	}
+
+	/* and values in the unsorted part must not be in sorted part */
+	for (i = ranges->nsorted; i < ranges->nvalues; i++)
+	{
+		compare_context cxt;
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nsorted == 0)
+			break;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		Assert(bsearch_arg(&value, &ranges->values[2 * ranges->nranges],
+						   ranges->nsorted, sizeof(Datum),
+						   compare_values, (void *) &cxt) == NULL);
+	}
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertCheckExpandedRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						  Form_pg_attribute attr, ExpandedRange *ranges,
+						  int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+	FmgrInfo   *eq;
+	FmgrInfo   *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum		r;
+		Datum		minval = ranges[i].minval;
+		Datum		maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else					/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e. upper <
+	 * lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges - 1; i++)
+	{
+		Datum		r;
+		Datum		maxval = ranges[i].maxval;
+		Datum		minval = ranges[i + 1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * enough space for the maximum number of values (so as not to have to do
+ * repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size		len;
+	Ranges	   *ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values); /* fixed header */
+	len += maxvalues * sizeof(Datum);	/* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+
+/*
+ * range_deduplicate_values
+ *		Deduplicate the part with values in the simple points.
+ *
+ * This is meant to be a cheaper way of reducing the size of the ranges. It
+ * does not touch the ranges, and only sorts the other values - it does not
+ * call the distance functions, which may be quite expensive, etc.
+ *
+ * We do know the values are not duplicate with the ranges, because we check
+ * that before adding a new value. Same for the sorted part of values.
+ */
+static void
+range_deduplicate_values(Ranges *range)
+{
+	int			i,
+				n;
+	int			start;
+	compare_context cxt;
+
+	/*
+	 * If there are no unsorted values, we're done (this probably can't
+	 * happen, as we're adding values to unsorted part).
+	 */
+	if (range->nsorted == range->nvalues)
+		return;
+
+	/* sort the values */
+	cxt.colloid = range->colloid;
+	cxt.cmpFn = range->cmp;
+
+	/* the values start right after the ranges (which are always sorted) */
+	start = 2 * range->nranges;
+
+	/*
+	 * XXX This might do a merge sort, to leverage that the first part of the
+	 * array is already sorted. If the sorted part is large, it might be quite
+	 * a bit faster.
+	 */
+	qsort_arg(&range->values[start],
+			  range->nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	n = 1;
+	for (i = 1; i < range->nvalues; i++)
+	{
+		/* same as preceding value, so store it */
+		if (compare_values(&range->values[start + i - 1],
+						   &range->values[start + i],
+						   (void *) &cxt) == 0)
+			continue;
+
+		range->values[start + n] = range->values[start + i];
+
+		n++;
+	}
+
+	/* now all the values are sorted */
+	range->nvalues = n;
+	range->nsorted = n;
+
+	AssertCheckRanges(range, range->cmp, range->colloid);
+}
+
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size		len;
+	int			nvalues;
+	SerializedRanges *serialized;
+	Oid			typid;
+	int			typlen;
+	bool		typbyval;
+
+	int			i;
+	char	   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nsorted >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+	Assert(range->target_maxvalues > 0);
+
+	/* at this point the range should be compacted to the target size */
+	Assert(2 * range->nranges + range->nvalues <= range->target_maxvalues);
+
+	Assert(range->target_maxvalues <= range->maxvalues);
+
+	/* range boundaries are always sorted */
+	Assert(range->nvalues >= range->nsorted);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2 * range->nranges + range->nvalues;
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges, data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)			/* varlena */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)		/* cstring */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else						/* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->target_maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation this
+	 * depends on the particular data type).
+	 */
+	ptr = serialized->data;		/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int			tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int			tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + len));
+	}
+
+	/* exact size */
+	Assert(ptr == ((char *) serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(int maxvalues, SerializedRanges *serialized)
+{
+	int			i,
+				nvalues;
+	char	   *ptr;
+	bool		typbyval;
+	int			typlen;
+
+	Ranges	   *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2 * serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+	Assert(serialized->maxvalues <= maxvalues);
+
+	range = minmax_multi_init(maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->nsorted = serialized->nvalues;
+	range->maxvalues = maxvalues;
+	range->target_maxvalues = serialized->maxvalues;
+
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need to copy
+	 * the values and will instead just point the values to the serialized
+	 * varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *) serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+/*
+ * compare_expanded_ranges
+ *	  Compare the expanded ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_expanded_ranges(const void *a, const void *b, void *arg)
+{
+	ExpandedRange *ra = (ExpandedRange *) a;
+	ExpandedRange *rb = (ExpandedRange *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * compare_values
+ *	  Compare the values.
+ */
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum	   *da = (Datum *) a;
+	Datum	   *db = (Datum *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *da, *db);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *db, *da);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Check if the new value matches one of the existing ranges.
+ */
+static bool
+has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges,
+				   Datum newval, AttrNumber attno, Oid typid)
+{
+	Datum		compar;
+
+	Datum		minvalue = ranges->values[0];
+	Datum		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+
+	/* binary search on ranges */
+	int			start,
+				end;
+
+	if (ranges->nranges == 0)
+		return false;
+
+	/*
+	 * Otherwise, need to compare the new value with boundaries of all the
+	 * ranges. First check if it's less than the absolute minimum, which is
+	 * the first value in the array.
+	 */
+	cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												   BTLessStrategyNumber);
+	compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+	/* smaller than the smallest value in the range list */
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * And now compare it to the existing maximum (last value in the data
+	 * array). But only if we haven't already ruled out a possible match in
+	 * the minvalue check.
+	 */
+	cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													  BTGreaterStrategyNumber);
+	compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * So we know it's in the general min/max, the question is whether it
+	 * falls in one of the ranges or gaps. We'll do a binary search on
+	 * individual ranges - for each range we check equality (value falls into
+	 * the range), and then check ranges either above or below the current
+	 * range.
+	 */
+	start = 0;					/* first range */
+	end = (ranges->nranges - 1);	/* last range */
+	while (true)
+	{
+		int			midpoint = (start + end) / 2;
+
+		/* this means we ran out of ranges in the last step */
+		if (start > end)
+			return false;
+
+		/* copy the min/max values from the ranges */
+		minvalue = ranges->values[2 * midpoint];
+		maxvalue = ranges->values[2 * midpoint + 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in this range */
+		if (DatumGetBool(compar))
+		{
+			end = (midpoint - 1);
+			continue;
+		}
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+		/* larger than the largest value in this range */
+		if (DatumGetBool(compar))
+		{
+			start = (midpoint + 1);
+			continue;
+		}
+
+		/* hey, we found a matching range */
+		return true;
+	}
+
+	return false;
+}
+
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * The last parameter (full) determines whether we need to search all the
+ * values, including the unsorted part. With full=false, the unsorted part
+ * is not searched, which may produce false negatives and duplicate values
+ * (in the unsorted part only), but when we're building the range that's
+ * fine - we'll deduplicate before serialization, and it can only happen
+ * if there already are unsorted values (so it was already modified).
+ *
+ * Serialized ranges don't have any unsorted values, so this can't cause
+ * false negatives during querying.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+					 AttrNumber attno, Form_pg_attribute attr,
+					 Ranges *ranges, Datum newval, bool full)
+{
+	int			i;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid))
+		return true;
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													BTEqualStrategyNumber);
+
+	/*
+	 * There is no matching range, so let's inspect the sorted values.
+	 *
+	 * We do a sequential search for small number of values, and binary search
+	 * once we have more than 16 values. This threshold is somewhat arbitrary,
+	 * as it depends on how expensive the comparison function is.
+	 *
+	 * XXX If we use the threshold here, maybe we should do the same thing in
+	 * has_matching_range? Or maybe we should do the bin search all the time?
+	 *
+	 * XXX We could use the same optimization as for ranges, to check if the
+	 * value is between min/max, to maybe rule out all sorted values without
+	 * having to inspect all of them.
+	 */
+	if (ranges->nsorted >= 16)
+	{
+		compare_context cxt;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		if (bsearch_arg(&newval, &ranges->values[2 * ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) != NULL)
+			return true;
+	}
+	else
+	{
+		for (i = 2 * ranges->nranges; i < 2 * ranges->nranges + ranges->nsorted; i++)
+		{
+			Datum		compar;
+
+			compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+			/* found an exact match */
+			if (DatumGetBool(compar))
+				return true;
+		}
+	}
+
+	/* If not asked to inspect the unsorted part, we're done. */
+	if (!full)
+		return false;
+
+	/* Inspect the unsorted part. */
+	for (i = 2 * ranges->nranges + ranges->nsorted; i < 2 * ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum		compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * Expand ranges from Ranges into ExpandedRange array. This expects the
+ * eranges to be pre-allocated and with the correct size - there needs to be
+ * (nranges + nvalues) elements.
+ *
+ * The order of expanded ranges is arbitrary. We do expand the ranges first,
+ * and this part is sorted. But then we expand the values, and this part may
+ * be unsorted.
+ */
+static void
+fill_expanded_ranges(ExpandedRange *eranges, int neranges, Ranges *ranges)
+{
+	int			idx;
+	int			i;
+
+	/* Check that the output array has the right size. */
+	Assert(neranges == (ranges->nranges + ranges->nvalues));
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * i];
+		eranges[idx].maxval = ranges->values[2 * i + 1];
+		eranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].maxval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	/* Did we produce the expected number of elements? */
+	Assert(idx == neranges);
+
+	return;
+}
+
+/*
+ * Sort and deduplicate expanded ranges.
+ *
+ * The ranges may be deduplicated - we're simply appending values, without
+ * checking for duplicates etc. So maybe the deduplication will reduce the
+ * number of ranges enough, and we won't have to compute the distances etc.
+ *
+ * Returns the number of expanded ranges.
+ */
+static int
+sort_expanded_ranges(FmgrInfo *cmp, Oid colloid,
+					 ExpandedRange *eranges, int neranges)
+{
+	int			n;
+	int			i;
+	compare_context cxt;
+
+	Assert(neranges > 0);
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/*
+	 * XXX We do qsort on all the values, but we could also leverage the fact
+	 * that some of the input data is already sorted (all the ranges and maybe
+	 * some of the points) and do merge sort.
+	 */
+	qsort_arg(eranges, neranges, sizeof(ExpandedRange),
+			  compare_expanded_ranges, (void *) &cxt);
+
+	/*
+	 * Deduplicate the ranges - simply compare each range to the preceding
+	 * one, and skip the duplicate ones.
+	 */
+	n = 1;
+	for (i = 1; i < neranges; i++)
+	{
+		/* if the current range is equal to the preceding one, do nothing */
+		if (!compare_expanded_ranges(&eranges[i - 1], &eranges[i], (void *) &cxt))
+			continue;
+
+		/* otherwise copy it to n-th place (if not already there) */
+		if (i != n)
+			memcpy(&eranges[n], &eranges[i], sizeof(ExpandedRange));
+
+		n++;
+	}
+
+	Assert((n > 0) && (n <= neranges));
+
+	return n;
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the expanded ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_overlapping_ranges(FmgrInfo *cmp, Oid colloid,
+						 ExpandedRange *eranges, int neranges)
+{
+	int			idx;
+
+	/* Merge ranges (idx) and (idx+1) if they overlap. */
+	idx = 0;
+	while (idx < (neranges - 1))
+	{
+		Datum		r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap if (minval
+		 * < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges are
+		 * ordered, so there are no more overlaps, because all the remaining
+		 * ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they overlap only partially.
+		 * So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].maxval);
+
+		if (DatumGetBool(r))
+			eranges[idx].maxval = eranges[idx + 1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectively of the
+		 * previous state).
+		 */
+		eranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the remaining
+		 * ranges by 1. There are neranges elements, and we need to move
+		 * elements from (idx+2). That means the number of elements to move is
+		 * [ncranges - (idx+2)].
+		 */
+		memmove(&eranges[idx + 1], &eranges[idx + 2],
+				(neranges - (idx + 2)) * sizeof(ExpandedRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range, as
+		 * it might overlap with additional ranges thanks to the merge).
+		 */
+		neranges--;
+	}
+
+	return neranges;
+}
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ * This is intentionally sorting the distances in descending order, i.e.
+ * the longer gaps will be at the front.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *) a;
+	DistanceValue *db = (DistanceValue *) b;
+
+	if (da->value < db->value)
+		return 1;
+	else if (da->value > db->value)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Given an array of expanded ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (max-min) for pairs
+ * of consecutive ranges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_expanded_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			ndistances;
+	DistanceValue *distances;
+
+	Assert(neranges >= 2);
+
+	ndistances = (neranges - 1);
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * ndistances);
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges so
+	 * that we can sort them once.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		Datum		a1,
+					a2,
+					r;
+
+		a1 = eranges[i].maxval;
+		a2 = eranges[i + 1].minval;
+
+		/* compute length of the gap (between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index of the gap the distance is for */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/*
+	 * Sort the distances in descending order, so that the longest gaps are at
+	 * the front.
+	 */
+	pg_qsort(distances, ndistances, sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds expanded ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).  This expanded
+ * representation makes the processing a bit easier, as it allows handling
+ * ranges and points the same way.
+ *
+ * We sort and deduplicate the expanded ranges - this is necessary, because
+ * the points may be unsorted. And moreover the two parts (ranges and
+ * points) are sorted on their own.
+ */
+static ExpandedRange *
+build_expanded_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					  int *nranges)
+{
+	int			neranges;
+	ExpandedRange *eranges;
+
+	/* both ranges and points are expanded into a separate element */
+	neranges = ranges->nranges + ranges->nvalues;
+
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges */
+	fill_expanded_ranges(eranges, neranges, ranges);
+
+	/* sort and deduplicate the expanded ranges */
+	neranges = sort_expanded_ranges(cmp, colloid, eranges, neranges);
+
+	/* remember how many cranges we built */
+	*nranges = neranges;
+
+	return eranges;
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Counts boundary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(ExpandedRange *cranges, int ncranges)
+{
+	int			i;
+	int			count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+#endif
+
+/*
+ * reduce_expanded_ranges
+ *		reduce the ranges until the number of values is low enough
+ *
+ * Combines ranges until the number of boundary values drops below the
+ * threshold specified by max_values. This happens by merging enough
+ * ranges by distance between them.
+ *
+ * Returns the number of result ranges.
+ *
+ * We simply use the global min/max and then add boundaries for enough
+ * largest gaps. Each gap adds 2 values, so we simply use (target/2-1)
+ * distances. Then we simply sort all the values - each two values are
+ * a boundary of a range (possibly collapsed).
+ *
+ * XXX Some of the ranges may be collapsed (i.e. the min/max values are
+ * equal), but we ignore that for now. We could repeat the process,
+ * adding a couple more gaps recursively.
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ *
+ * XXX Or maybe we could just handle this by using random value as a
+ * tie-break, or by adding random noise to the actual distance.
+ */
+static int
+reduce_expanded_ranges(ExpandedRange *eranges, int neranges,
+					   DistanceValue *distances, int max_values,
+					   FmgrInfo *cmp, Oid colloid)
+{
+	int			i;
+	int			nvalues;
+	Datum	   *values;
+
+	compare_context cxt;
+
+	/* total number of gaps between ranges */
+	int			ndistances = (neranges - 1);
+
+	/* number of gaps to keep */
+	int			keep = (max_values / 2 - 1);
+
+	/*
+	 * Maybe we have sufficiently low number of ranges already?
+	 *
+	 * XXX This should happen before we actually do the expensive stuff like
+	 * sorting, so maybe this should be just an assert.
+	 */
+	if (keep >= ndistances)
+		return neranges;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	/* add the global min/max values, from the first/last range */
+	values[nvalues++] = eranges[0].minval;
+	values[nvalues++] = eranges[neranges - 1].maxval;
+
+	/* add boundary values for enough gaps */
+	for (i = 0; i < keep; i++)
+	{
+		/* index of the gap between (index) and (index+1) ranges */
+		int			index = distances[i].index;
+
+		Assert((index >= 0) && ((index + 1) < neranges));
+
+		/* add max from the preceding range, minval from the next one */
+		values[nvalues++] = eranges[index].maxval;
+		values[nvalues++] = eranges[index + 1].minval;
+
+		Assert(nvalues <= max_values);
+	}
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/*
+	 * Sort the values using the comparator function, and form ranges from the
+	 * sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		eranges[i].minval = values[2 * i];
+		eranges[i].maxval = values[2 * i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		eranges[i].collapsed = (compare_values(&values[2 * i],
+											   &values[2 * i + 1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+/*
+ * Store the boundary values from ExpandedRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_expanded_ranges(Ranges *ranges, ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (!eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->values[idx++] = eranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+
+	/* all the values are sorted */
+	ranges->nsorted = ranges->nvalues;
+
+	Assert(count_values(eranges, neranges) == 2 * ranges->nranges + ranges->nvalues);
+	Assert(2 * ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+}
+
+
+/*
+ * Consider freeing space in the ranges. Checks if there's space for at least
+ * one new value, and performs compaction if needed.
+ *
+ * Returns true if the value was actually modified.
+ */
+static bool
+ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *range)
+{
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	/*
+	 * If there is free space in the buffer, we're done without having to
+	 * modify anything.
+	 */
+	if (2 * range->nranges + range->nvalues < range->maxvalues)
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/*
+	 * did we reduce enough free space by just the deduplication?
+	 *
+	 * We don't simply check against range->maxvalues again. The deduplication
+	 * might have freed very little space (e.g. just one value), forcing us to
+	 * do depuplication very often. In that case it's better to do compaction
+	 * and reduce more space.
+	 */
+	if (2 * range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR)
+		return true;
+
+	/*
+	 * We need to combine some of the existing ranges, to reduce the number of
+	 * values we have to store.
+	 *
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it (we might have
+	 * to do this repeatedly, even for a single BRIN page range). Otherwise
+	 * we'd have problems e.g. when building new indexes. So we use a memory
+	 * context and make sure we free the memory at the end (so if we call the
+	 * distance function many times, it might be an issue, but meh).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, colloid, range, &neranges);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * Combine ranges until we release at least 50% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not use
+	 * too low or high value.
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR,
+									  cmpFn, colloid);
+
+	/* Make sure we've sufficiently reduced the number of ranges. */
+	Assert(count_values(eranges, neranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR);
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(range, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Did we break the ranges somehow? */
+	AssertCheckRanges(range, cmpFn, colloid);
+
+	return true;
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the minmax-multi range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn;
+	bool		modified = false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Make sure there's enough free space in the buffer. We only trigger this
+	 * when the buffer is full, which means it had to be modified as we size
+	 * it to be larger than what is stored on disk.
+	 *
+	 * This needs to happen before we check if the value is contained in the
+	 * range, because the value might be in the unsorted part, and we don't
+	 * check that in range_contains_value. The deduplication would then move
+	 * it to the sorted part, and we'd add the value too, which violates the
+	 * rule that we never have duplicates with the ranges or sorted values.
+	 *
+	 * We might also deduplicate and recheck if the value is contained, but
+	 * that seems like an overkill. We'd need to deduplicate anyway, so why
+	 * not do it now.
+	 */
+	modified = ensure_free_space_in_buffer(bdesc, colloid,
+										   attno, attr, ranges);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then do the
+	 * deduplication in a batch, hoping for better efficiency. But that would
+	 * mean we actually modify the range every time, which means having to
+	 * serialize the value, which does palloc, walks the values, copies them,
+	 * etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming the
+	 * comparator function is not very expensive.
+	 *
+	 * This also implies the values array can't contain duplicate values.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval, false))
+		return modified;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we do a
+	 * simple insertion sort. We could do something more elaborate, e.g. by
+	 * sorting the values only now and then, but for small counts (e.g. when
+	 * maxvalues is 64) this should be fine.
+	 */
+	ranges->values[2 * ranges->nranges + ranges->nvalues] = newval;
+	ranges->nvalues++;
+
+	/* If we added the first value, we can consider it as sorted. */
+	if (ranges->nvalues == 1)
+		ranges->nsorted = 1;
+
+	/*
+	 * Check we haven't broken the ordering of boundary values (checks both
+	 * parts, but that doesn't hurt).
+	 */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/* Check the range contains the value we just added. */
+	Assert(range_contains_value(bdesc, colloid, attno, attr, ranges, newval, true));
+
+	/* yep, we've modified the range */
+	return true;
+}
+
+/*
+ * Generate range representation of data collected during "batch mode".
+ * This is similar to reduce_expanded_ranges, except that we can't assume
+ * the values are sorted and there may be duplicate values.
+ */
+static void
+compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	/*
+	 * Do we need to actually compactify anything?
+	 *
+	 * There are two reasons why compaction may be needed - firstly, there may
+	 * be too many values, or some of the values may be unsorted.
+	 */
+	if ((ranges->nranges * 2 + ranges->nvalues <= max_values) &&
+		(ranges->nsorted == ranges->nvalues))
+		return;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
+											   BTLessStrategyNumber);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, ranges->attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, ranges->colloid, ranges, &neranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, ranges->colloid,
+								eranges, neranges);
+
+	/*
+	 * Combine ranges until we get below max_values. We don't use any scale
+	 * factor, because this is used during serialization, and we don't expect
+	 * more tuples to be inserted anytime soon.
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  max_values, cmpFn, ranges->colloid);
+
+	Assert(count_values(eranges, neranges) <= max_values);
+
+	/* transform back into regular ranges and single values */
+	store_expanded_ranges(ranges, eranges, neranges);
+
+	/* check all the range invariants */
+	AssertCheckRanges(ranges, cmpFn, ranges->colloid);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float		a1 = PG_GETARG_FLOAT4(0);
+	float		a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double		a1 = PG_GETARG_FLOAT8(0);
+	double		a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16		a1 = PG_GETARG_INT16(0);
+	int16		a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32		a1 = PG_GETARG_INT32(0);
+	int32		a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64		a1 = PG_GETARG_INT64(0);
+	int64		a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double		da1,
+				da2;
+
+	ItemPointer pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	/*
+	 * We use the no-check variants here, because user-supplied values may
+	 * have (ip_posid == 0). See ItemPointerCompare.
+	 */
+	da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa1);
+
+	da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Computes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum		d;
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int			i;
+	float8		delta = 0;
+
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN - 1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute approximate distance between two dates.
+ */
+Datum
+brin_minmax_multi_distance_date(PG_FUNCTION_ARGS)
+{
+	DateADT		dateVal1 = PG_GETARG_DATEADT(0);
+	DateADT		dateVal2 = PG_GETARG_DATEADT(1);
+
+	if (DATE_NOT_FINITE(dateVal1) || DATE_NOT_FINITE(dateVal2))
+		PG_RETURN_FLOAT8(0);
+
+	PG_RETURN_FLOAT8(dateVal1 - dateVal2);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeADT		ta = PG_GETARG_TIMEADT(0);
+	TimeADT		tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+Datum
+brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Timestamp	dt1 = PG_GETARG_TIMESTAMP(0);
+	Timestamp	dt2 = PG_GETARG_TIMESTAMP(1);
+
+	if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
+		PG_RETURN_FLOAT8(0);
+
+	delta = dt2 - dt1;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Interval   *ia = PG_GETARG_INTERVAL_P(0);
+	Interval   *ib = PG_GETARG_INTERVAL_P(1);
+	Interval   *result;
+
+	result = (Interval *) palloc(sizeof(Interval));
+
+	result->month = ib->month - ia->month;
+	/* overflow check copied from int4mi */
+	if (!SAMESIGN(ib->month, ia->month) &&
+		!SAMESIGN(result->month, ib->month))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->day = ib->day - ia->day;
+	if (!SAMESIGN(ib->day, ia->day) &&
+		!SAMESIGN(result->day, ib->day))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->time = ib->time - ia->time;
+	if (!SAMESIGN(ib->time, ia->time) &&
+		!SAMESIGN(result->time, ib->time))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	/*
+	 * We assume months have 31 days - we don't need to be precise, in the
+	 * worst case we'll build somewhat less efficient ranges.
+	 */
+	delta = (float8) (result->month * 31 + result->day);
+
+	/* convert to microseconds (just like the time part) */
+	delta = 24L * 3600L * delta;
+
+	/* and add the time part */
+	delta += result->time / (float8) 1000000.0;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((float8) b->h - (float8) a->h);
+	delta /= 256;
+
+	delta += ((float8) b->g - (float8) a->g);
+	delta /= 256;
+
+	delta += ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from different families are considered to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+	int			i;
+	int			len;
+	unsigned char *addra,
+			   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be in
+	 * maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		PG_RETURN_FLOAT8(1.0);
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16;				/* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len - 1; i >= 0; i--)
+	{
+		delta += (float8) addrb[i] - (float8) addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
+{
+	Ranges	   *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s;
+
+	/*
+	 * In batch mode, we need to compress the accumulated values to the
+	 * actually requested number of values/ranges.
+	 */
+	compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+
+	/* At this point everything has to be fully sorted. */
+	Assert(ranges->nsorted == ranges->nvalues);
+
+	s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxMultiOptions *opts)
+{
+	return MinMaxMultiGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxMultiOptions *opts = (MinMaxMultiOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges	   *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, if possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 *
+	 * When starting with an empty range, we assume this is a batch mode and
+	 * we use a larger buffer. The buffer size is derived from the BRIN range
+	 * size, number of rows per page, with some sensible min/max values. Small
+	 * buffer would be bad for performance, but large buffer might require a
+	 * lot of memory (because of keeping all the values).
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		int			target_maxvalues;
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		/* what was specified as a reloption? */
+		target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, target_maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+		ranges = minmax_multi_init(maxvalues);
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+		ranges->target_maxvalues = target_maxvalues;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, serialized->maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		ranges = range_deserialize(maxvalues, serialized);
+
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges	   *ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized->maxvalues, serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum		minval = ranges->values[2 * rangeno];
+		Datum		maxval = ranges->values[2 * rangeno + 1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+					{
+						Datum		compar;
+						FmgrInfo   *cmpFn;
+
+						/* by default this range does not match */
+						matches = false;
+
+						/*
+						 * Otherwise, need to compare the new value with
+						 * boundaries of all the ranges. First check if it's
+						 * less than the absolute minimum, which is the first
+						 * value in the array.
+						 */
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTLessStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+						/* smaller than the smallest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTGreaterStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+						/* larger than the largest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						/*
+						 * haven't managed to eliminate this range, so
+						 * consider it matching
+						 */
+						matches = true;
+
+						break;
+					}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/*
+	 * And now inspect the values. We don't bother with doing a binary search
+	 * here, because we're dealing with serialized / fully compacted ranges,
+	 * so there should be only very few values.
+	 */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		val = ranges->values[2 * ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges *serialized_a;
+	SerializedRanges *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	ExpandedRange *eranges;
+	int			neranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue *distances;
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a);
+	ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	neranges = (ranges_a->nranges + ranges_a->nvalues) +
+		(ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges with entries for the first range */
+	fill_expanded_ranges(eranges, ranges_a->nranges + ranges_a->nvalues,
+						 ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_expanded_ranges(&eranges[ranges_a->nranges + ranges_a->nvalues],
+						 ranges_b->nranges + ranges_b->nvalues,
+						 ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* sort the expanded ranges */
+	sort_expanded_ranges(cmpFn, colloid, eranges, neranges);
+
+	/*
+	 * We've loaded two different lists of expanded ranges, so some of them
+	 * may be overlapping. So walk through them and merge them.
+	 */
+	neranges = merge_overlapping_ranges(cmpFn, colloid, eranges, neranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertCheckExpandedRanges(bdesc, colloid, attno, attr, eranges, neranges);
+
+	/*
+	 * If needed, reduce some of the ranges.
+	 *
+	 * XXX This may be fairly expensive, so maybe we should do it only when
+	 * it's actually needed (when we have too many ranges).
+	 */
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many of them to get below the threshold. The
+	 * collapsed ranges will be stored as a single value.
+	 *
+	 * XXX This does not apply the load factor, as we don't expect to add more
+	 * values to the range, so we prefer to keep as many ranges as possible.
+	 *
+	 * XXX Can the maxvalues be different in the two ranges? Perhaps we should
+	 * use maximum of those?
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  ranges_a->maxvalues,
+									  cmpFn, colloid);
+
+	/* update the first range summary */
+	store_expanded_ranges(ranges_a, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+								   uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxMultiOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							MINMAX_MULTI_DEFAULT_VALUES_PER_PAGE, 8, 256,
+							offsetof(MinMaxMultiOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges	   *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges->maxvalues, ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum		a,
+					b;
+		text	   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum		a;
+		text	   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 8d03e609a3..baf87bdcae 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* If needed, serialize the values before forming the on-disk tuple. */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(brdesc,
+												  tuple->bt_columns[keyno].bv_mem_value,
+												  tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -193,9 +201,9 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 * If value is stored EXTERNAL, must fetch it so we are not
 			 * depending on outside storage.
 			 *
-			 * XXX Is this actually true? Could it be that the summary is
-			 * NULL even for range with non-NULL data? E.g. degenerate bloom
-			 * filter may be thrown away, etc.
+			 * XXX Is this actually true? Could it be that the summary is NULL
+			 * even for range with non-NULL data? E.g. degenerate bloom filter
+			 * may be thrown away, etc.
 			 */
 			if (VARATT_IS_EXTERNAL(DatumGetPointer(value)))
 			{
@@ -510,6 +518,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -589,6 +602,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..87de94f397 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,11 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+/*
+ * The BRIN opclasses may register serialization callback, in case the on-disk
+ * and in-memory representations differ (e.g. for performance reasons).
+ */
+typedef void (*brin_serialize_callback_type) (BrinDesc *bdesc, Datum src, Datum *dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +32,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 82e874130d..2f7338ee82 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -161,18 +161,18 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  *		development purposes (such as in-progress patches and forks);
  *		they should not appear in released versions.
  *
- *		OIDs 10000-11999 are reserved for assignment by genbki.pl, for use
+ *		OIDs 10000-12999 are reserved for assignment by genbki.pl, for use
  *		when the .dat files in src/include/catalog/ do not specify an OID
  *		for a catalog entry that requires one.
  *
- *		OIDS 12000-16383 are reserved for assignment during initdb
- *		using the OID generator.  (We start the generator at 12000.)
+ *		OIDS 13000-16383 are reserved for assignment during initdb
+ *		using the OID generator.  (We start the generator at 13000.)
  *
  *		OIDs beginning at 16384 are assigned from the OID generator
  *		during normal multiuser operation.  (We force the generator up to
  *		16384 as soon as we are in normal operation.)
  *
- * The choices of 8000, 10000 and 12000 are completely arbitrary, and can be
+ * The choices of 8000, 10000 and 13000 are completely arbitrary, and can be
  * moved if we run low on OIDs in any category.  Changing the macros below,
  * and updating relevant documentation (see bki.sgml and RELEASE_CHANGES),
  * should be sufficient to do this.  Moving the 16384 boundary between
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 017ef60217..f007a2d9ef 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202103222
+#define CATALOG_VERSION_NO	202103223
 
 #endif
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 04d678f96a..8135854163 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2062,6 +2208,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2088,6 +2251,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2155,6 +2334,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2180,6 +2425,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2202,6 +2464,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2224,6 +2503,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2288,6 +2584,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2439,6 +2752,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2470,6 +2929,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2492,6 +2968,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2548,6 +3041,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2570,6 +3080,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2636,6 +3163,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 2af8af3f4e..f2c0ccfa1f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 7cc3d59a8c..04edca6cb0 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '4602',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '4572',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '4603',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '4573',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '4604',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '4575',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '4605',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '4576',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '4606',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '4580',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '4581',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '4607',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '4608',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '4582',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '4609',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '4583',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '4610',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '4584',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '4611',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '4585',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '4612',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '4587',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '4613',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '4588',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '4614',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '4589',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '4615',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '4590',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c89df733f7..183358c1ba 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8221,6 +8221,77 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '4616', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '4617', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '4618', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '4619', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '4620', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '4621', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '4622', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '4623', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '4624', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '4625', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '4626', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '4627', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '4628', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '4629', descr => 'BRIN multi minmax date distance',
+  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
+{ oid => '4630', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '4631', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '4632', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '4633', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '4634', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '4635', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '4636', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+{ oid => '4637', descr => 'BRIN multi minmax timestamp distance',
+  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11445,4 +11516,18 @@
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
 
+{ oid => '4638', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '4639', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '4640', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '4641', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 2a82a3e544..8c145c00be 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -685,4 +685,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+{ oid => '4601',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..0a7e4b8060
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,450 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+ERROR:  value 7 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+DROP INDEX brinidx_multi;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- test building an index with many values, to force compaction of the buffer
+CREATE TABLE brin_large_range (a int4);
+INSERT INTO brin_large_range SELECT i FROM generate_series(1,10000) s(i);
+CREATE INDEX brin_large_range_idx ON brin_large_range USING brin (a int4_minmax_multi_ops);
+DROP TABLE brin_large_range;
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 48ce3f7411..5480f979c6 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  4600 | pg_brin_bloom_summary
+ 4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(5 rows)
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 95927a7bae..c6e49affeb 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index c02a981c78..1d7eb418a7 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..f5ffcaea33
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,403 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+
+DROP INDEX brinidx_multi;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- test building an index with many values, to force compaction of the buffer
+CREATE TABLE brin_large_range (a int4);
+INSERT INTO brin_large_range SELECT i FROM generate_series(1,10000) s(i);
+CREATE INDEX brin_large_range_idx ON brin_large_range USING brin (a int4_minmax_multi_ops);
+DROP TABLE brin_large_range;
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.30.2

0007-move-bloom-to-contrib-20210322.patchtext/x-patch; charset=UTF-8; name=0007-move-bloom-to-contrib-20210322.patchDownload
From 46538a30090bc4c56d21780fe5d7ca243741134d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Mon, 22 Mar 2021 03:22:41 +0100
Subject: [PATCH 7/8] move bloom to contrib

---
 contrib/Makefile                              |   1 +
 contrib/brin_bloom/.gitignore                 |   4 +
 contrib/brin_bloom/Makefile                   |  23 +
 contrib/brin_bloom/brin_bloom--1.0.sql        | 350 ++++++++++++++
 .../brin => contrib/brin_bloom}/brin_bloom.c  |  17 +-
 contrib/brin_bloom/brin_bloom.control         |   6 +
 .../brin_bloom}/expected/brin_bloom.out       |  21 +
 .../brin_bloom}/sql/brin_bloom.sql            |  25 +
 src/backend/access/brin/Makefile              |   1 -
 src/include/catalog/pg_amop.dat               | 115 -----
 src/include/catalog/pg_amproc.dat             | 447 ------------------
 src/include/catalog/pg_opclass.dat            |  72 ---
 src/include/catalog/pg_opfamily.dat           |  38 --
 src/include/catalog/pg_proc.dat               |  34 --
 src/include/catalog/pg_type.dat               |   6 -
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 -
 17 files changed, 447 insertions(+), 716 deletions(-)
 create mode 100644 contrib/brin_bloom/.gitignore
 create mode 100644 contrib/brin_bloom/Makefile
 create mode 100644 contrib/brin_bloom/brin_bloom--1.0.sql
 rename {src/backend/access/brin => contrib/brin_bloom}/brin_bloom.c (98%)
 create mode 100644 contrib/brin_bloom/brin_bloom.control
 rename {src/test/regress => contrib/brin_bloom}/expected/brin_bloom.out (96%)
 rename {src/test/regress => contrib/brin_bloom}/sql/brin_bloom.sql (96%)

diff --git a/contrib/Makefile b/contrib/Makefile
index f27e458482..f878287429 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -12,6 +12,7 @@ SUBDIRS = \
 		bloom		\
 		btree_gin	\
 		btree_gist	\
+		brin_bloom	\
 		citext		\
 		cube		\
 		dblink		\
diff --git a/contrib/brin_bloom/.gitignore b/contrib/brin_bloom/.gitignore
new file mode 100644
index 0000000000..5dcb3ff972
--- /dev/null
+++ b/contrib/brin_bloom/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/brin_bloom/Makefile b/contrib/brin_bloom/Makefile
new file mode 100644
index 0000000000..d8ca383cf1
--- /dev/null
+++ b/contrib/brin_bloom/Makefile
@@ -0,0 +1,23 @@
+# contrib/brin_bloom/Makefile
+
+MODULE_big = brin_bloom
+OBJS = \
+	$(WIN32RES) \
+	brin_bloom.o
+
+EXTENSION = brin_bloom
+DATA = brin_bloom--1.0.sql
+PGFILEDESC = "brin_bloom - BRIN bloom operator classes"
+
+REGRESS = brin_bloom
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/brin_bloom
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/brin_bloom/brin_bloom--1.0.sql b/contrib/brin_bloom/brin_bloom--1.0.sql
new file mode 100644
index 0000000000..29a9a88de0
--- /dev/null
+++ b/contrib/brin_bloom/brin_bloom--1.0.sql
@@ -0,0 +1,350 @@
+/* contrib/brin_bloom/brin_bloom--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION brin_bloom" to load this file. \quit
+
+CREATE TYPE brin_bloom_summary;
+
+-- BRIN bloom summary data type
+CREATE FUNCTION brin_bloom_summary_in(cstring)
+RETURNS brin_bloom_summary
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_bloom_summary_out(brin_bloom_summary)
+RETURNS cstring
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_bloom_summary_recv(internal)
+RETURNS brin_bloom_summary
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_bloom_summary_send(brin_bloom_summary)
+RETURNS bytea
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE TYPE brin_bloom_summary (
+	INTERNALLENGTH = -1,
+	INPUT = brin_bloom_summary_in,
+	OUTPUT = brin_bloom_summary_out,
+	RECEIVE = brin_bloom_summary_recv,
+	SEND = brin_bloom_summary_send,
+	STORAGE = extended
+);
+
+-- BRIN support procedures
+CREATE FUNCTION brin_bloom_opcinfo(internal)
+RETURNS internal
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_bloom_add_value(internal, internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_bloom_consistent(internal, internal, internal, int4)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_bloom_union(internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_bloom_options(internal)
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE OPERATOR CLASS bpchar_bloom_ops
+FOR TYPE bpchar USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashbpchar(bpchar),
+STORAGE         bpchar;
+
+CREATE OPERATOR CLASS bytea_bloom_ops
+FOR TYPE bytea USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashvarlena(internal),
+STORAGE         bytea;
+
+CREATE OPERATOR CLASS char_bloom_ops
+FOR TYPE "char" USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashchar("char"),
+STORAGE         "char";
+
+CREATE OPERATOR CLASS date_bloom_ops
+FOR TYPE date USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashint4(int4),
+STORAGE         date;
+
+CREATE OPERATOR CLASS float4_bloom_ops
+FOR TYPE float4 USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashfloat4(float4),
+STORAGE         float4;
+
+CREATE OPERATOR CLASS float8_bloom_ops
+FOR TYPE float8 USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashfloat8(float8),
+STORAGE         float8;
+
+CREATE OPERATOR CLASS inet_bloom_ops
+FOR TYPE inet USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashinet(inet),
+STORAGE         inet;
+
+CREATE OPERATOR CLASS int2_bloom_ops
+FOR TYPE int2 USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashint2(int2),
+STORAGE         int2;
+
+CREATE OPERATOR CLASS int4_bloom_ops
+FOR TYPE int4 USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashint4(int4),
+STORAGE         int4;
+
+CREATE OPERATOR CLASS int8_bloom_ops
+FOR TYPE int8 USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashint8(int8),
+STORAGE         int8;
+
+CREATE OPERATOR CLASS interval_bloom_ops
+FOR TYPE interval USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      interval_hash(interval),
+STORAGE         interval;
+
+CREATE OPERATOR CLASS macaddr_bloom_ops
+FOR TYPE macaddr USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashmacaddr(macaddr),
+STORAGE         macaddr;
+
+CREATE OPERATOR CLASS macaddr8_bloom_ops
+FOR TYPE macaddr8 USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashmacaddr8(macaddr8),
+STORAGE         macaddr8;
+
+CREATE OPERATOR CLASS name_bloom_ops
+FOR TYPE name USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashname(name),
+STORAGE         name;
+
+CREATE OPERATOR CLASS numeric_bloom_ops
+FOR TYPE numeric USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hash_numeric(numeric),
+STORAGE         numeric;
+
+CREATE OPERATOR CLASS oid_bloom_ops
+FOR TYPE oid USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashoid(oid),
+STORAGE         oid;
+
+CREATE OPERATOR CLASS pg_lsn_bloom_ops
+FOR TYPE pg_lsn USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      pg_lsn_hash(pg_lsn),
+STORAGE         pg_lsn;
+
+CREATE OPERATOR CLASS text_bloom_ops
+FOR TYPE text USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashtext(text),
+STORAGE         text;
+
+CREATE OPERATOR CLASS tid_bloom_ops
+FOR TYPE tid USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashtid(tid),
+STORAGE         tid;
+
+CREATE OPERATOR CLASS time_bloom_ops
+FOR TYPE time USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      time_hash(time),
+STORAGE         time;
+
+CREATE OPERATOR CLASS timestamp_bloom_ops
+FOR TYPE timestamp USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      timestamp_hash(timestamp),
+STORAGE         timestamp;
+
+CREATE OPERATOR CLASS timestamptz_bloom_ops
+FOR TYPE timestamptz USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      timestamp_hash(timestamp),
+STORAGE         timestamptz;
+
+CREATE OPERATOR CLASS timetz_bloom_ops
+FOR TYPE timetz USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      timetz_hash(timetz),
+STORAGE         timetz;
+
+CREATE OPERATOR CLASS uuid_bloom_ops
+FOR TYPE uuid USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      uuid_hash(uuid),
+STORAGE         uuid;
diff --git a/src/backend/access/brin/brin_bloom.c b/contrib/brin_bloom/brin_bloom.c
similarity index 98%
rename from src/backend/access/brin/brin_bloom.c
rename to contrib/brin_bloom/brin_bloom.c
index 2214fb4d0c..1a8e6d5337 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/contrib/brin_bloom/brin_bloom.c
@@ -133,6 +133,9 @@
 
 #include <math.h>
 
+PG_MODULE_MAGIC;
+
+
 #define BloomEqualStrategyNumber	1
 
 /*
@@ -225,6 +228,18 @@ typedef struct BloomOptions
 #define BLOOM_SEED_1	0x71d924af
 #define BLOOM_SEED_2	0xba48b314
 
+
+PG_FUNCTION_INFO_V1(brin_bloom_summary_in);
+PG_FUNCTION_INFO_V1(brin_bloom_summary_out);
+PG_FUNCTION_INFO_V1(brin_bloom_summary_send);
+PG_FUNCTION_INFO_V1(brin_bloom_summary_recv);
+
+PG_FUNCTION_INFO_V1(brin_bloom_opcinfo);
+PG_FUNCTION_INFO_V1(brin_bloom_options);
+PG_FUNCTION_INFO_V1(brin_bloom_add_value);
+PG_FUNCTION_INFO_V1(brin_bloom_consistent);
+PG_FUNCTION_INFO_V1(brin_bloom_union);
+
 /*
  * Bloom Filter
  *
@@ -438,7 +453,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
-	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
 
 	PG_RETURN_POINTER(result);
 }
diff --git a/contrib/brin_bloom/brin_bloom.control b/contrib/brin_bloom/brin_bloom.control
new file mode 100644
index 0000000000..b77048a4ac
--- /dev/null
+++ b/contrib/brin_bloom/brin_bloom.control
@@ -0,0 +1,6 @@
+# brin_bloom extension
+comment = 'support for BRIN bloom indexes'
+default_version = '1.0'
+module_pathname = '$libdir/brin_bloom'
+relocatable = true
+trusted = true
diff --git a/src/test/regress/expected/brin_bloom.out b/contrib/brin_bloom/expected/brin_bloom.out
similarity index 96%
rename from src/test/regress/expected/brin_bloom.out
rename to contrib/brin_bloom/expected/brin_bloom.out
index 32c56a996a..4544d09691 100644
--- a/src/test/regress/expected/brin_bloom.out
+++ b/contrib/brin_bloom/expected/brin_bloom.out
@@ -1,3 +1,24 @@
+CREATE EXTENSION brin_bloom;
+CREATE TABLE tenk1 (
+	unique1		int4,
+	unique2		int4,
+	two			int4,
+	four		int4,
+	ten			int4,
+	twenty		int4,
+	hundred		int4,
+	thousand	int4,
+	twothousand	int4,
+	fivethous	int4,
+	tenthous	int4,
+	odd			int4,
+	even		int4,
+	stringu1	name,
+	stringu2	name,
+	string4		name
+);
+\copy tenk1 from '../../src/test/regress/data/tenk.data'
+CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops);
 CREATE TABLE brintest_bloom (byteacol bytea,
 	charcol "char",
 	namecol name,
diff --git a/src/test/regress/sql/brin_bloom.sql b/contrib/brin_bloom/sql/brin_bloom.sql
similarity index 96%
rename from src/test/regress/sql/brin_bloom.sql
rename to contrib/brin_bloom/sql/brin_bloom.sql
index 5d499208e3..fa79053ac5 100644
--- a/src/test/regress/sql/brin_bloom.sql
+++ b/contrib/brin_bloom/sql/brin_bloom.sql
@@ -1,3 +1,28 @@
+CREATE EXTENSION brin_bloom;
+
+CREATE TABLE tenk1 (
+	unique1		int4,
+	unique2		int4,
+	two			int4,
+	four		int4,
+	ten			int4,
+	twenty		int4,
+	hundred		int4,
+	thousand	int4,
+	twothousand	int4,
+	fivethous	int4,
+	tenthous	int4,
+	odd			int4,
+	even		int4,
+	stringu1	name,
+	stringu2	name,
+	string4		name
+);
+
+\copy tenk1 from '../../src/test/regress/data/tenk.data'
+
+CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops);
+
 CREATE TABLE brintest_bloom (byteacol bytea,
 	charcol "char",
 	namecol name,
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index a386cb71f1..75eb87ec10 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,7 +14,6 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
-	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_minmax_multi.o \
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 8135854163..51bef55a57 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,11 +1814,6 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
-# bloom bytea
-{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
-  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
-  amopmethod => 'brin' },
-
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1836,11 +1831,6 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
-# bloom "char"
-{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
-  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
-  amopmethod => 'brin' },
-
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1858,11 +1848,6 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
-# bloom name
-{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
-  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
-  amopmethod => 'brin' },
-
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -2155,20 +2140,6 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
-# bloom integer
-
-{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
-  amopmethod => 'brin' },
-
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2186,11 +2157,6 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
-# bloom text
-{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
-  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
-  amopmethod => 'brin' },
-
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2225,11 +2191,6 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
-# bloom oid
-{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
-  amopmethod => 'brin' },
-
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2247,10 +2208,6 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
-# tid oid
-{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
-  amopmethod => 'brin' },
 # minmax multi tid
 { amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2400,14 +2357,6 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
-# bloom float
-{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
-  amopmethod => 'brin' },
-
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2442,11 +2391,6 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
-# bloom macaddr
-{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '1',
-  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
-
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2481,11 +2425,6 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
-# bloom macaddr8
-{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '1',
-  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
-
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2520,11 +2459,6 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
-# bloom inet
-{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
-  amopmethod => 'brin' },
-
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2562,11 +2496,6 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
-# bloom character
-{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
-  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
-  amopmethod => 'brin' },
-
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2601,11 +2530,6 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
-# bloom time without time zone
-{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
-  amopmethod => 'brin' },
-
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2898,20 +2822,6 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
-# bloom datetime (date, timestamp, timestamptz)
-
-{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '1',
-  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '1',
-  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
-
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2946,11 +2856,6 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
-# bloom interval
-{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '1',
-  amopopr => '=(interval,interval)', amopmethod => 'brin' },
-
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2985,11 +2890,6 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
-# bloom time with time zone
-{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
-  amopmethod => 'brin' },
-
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -3058,11 +2958,6 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
-# bloom numeric
-{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '1',
-  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
-
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -3097,11 +2992,6 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
-# bloom uuid
-{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
-  amopmethod => 'brin' },
-
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -3180,11 +3070,6 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
-# bloom pg_lsn
-{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
-  amopmethod => 'brin' },
-
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index f2c0ccfa1f..68c3342eeb 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,24 +805,6 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# bloom bytea
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
-
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -836,24 +818,6 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# bloom "char"
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
-
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -867,24 +831,6 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# bloom name
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
-
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1132,58 +1078,6 @@
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
 
-# bloom integer: int2, int4, int8
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
-
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
-
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
-
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -1197,24 +1091,6 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# bloom text
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
-
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1244,23 +1120,6 @@
 { amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
 
-# bloom oid
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
-
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1273,23 +1132,6 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# bloom tid
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
-
 # minmax multi tid
 { amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
@@ -1431,45 +1273,6 @@
   amprocrighttype => 'float4', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_float8' },
 
-# bloom float
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '11',
-  amproc => 'hashfloat4' },
-
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '11',
-  amproc => 'hashfloat8' },
-
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1504,26 +1307,6 @@
   amprocrighttype => 'macaddr', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_macaddr' },
 
-# bloom macaddr
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '11',
-  amproc => 'hashmacaddr' },
-
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1558,26 +1341,6 @@
   amprocrighttype => 'macaddr8', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_macaddr8' },
 
-# bloom macaddr8
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '11',
-  amproc => 'hashmacaddr8' },
-
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1611,24 +1374,6 @@
   amprocrighttype => 'inet', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_inet' },
 
-# bloom inet
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
-
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1663,26 +1408,6 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# bloom character
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '11',
-  amproc => 'hashbpchar' },
-
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1715,24 +1440,6 @@
   amprocrighttype => 'time', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_time' },
 
-# bloom time without time zone
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
-
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -2004,62 +1711,6 @@
   amprocrighttype => 'timestamptz', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_date' },
 
-# bloom datetime (date, timestamp, timestamptz)
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '11',
-  amproc => 'timestamp_hash' },
-
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '11',
-  amproc => 'timestamp_hash' },
-
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
-
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -2094,26 +1745,6 @@
   amprocrighttype => 'interval', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_interval' },
 
-# bloom interval
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '11',
-  amproc => 'interval_hash' },
-
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -2148,26 +1779,6 @@
   amprocrighttype => 'timetz', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_timetz' },
 
-# bloom time with time zone
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '11',
-  amproc => 'timetz_hash' },
-
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -2228,26 +1839,6 @@
   amprocrighttype => 'numeric', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_numeric' },
 
-# bloom numeric
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '11',
-  amproc => 'hash_numeric' },
-
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -2282,24 +1873,6 @@
   amprocrighttype => 'uuid', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_uuid' },
 
-# bloom uuid
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
-
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -2357,26 +1930,6 @@
   amprocrighttype => 'pg_lsn', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_pg_lsn' },
 
-# bloom pg_lsn
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '11',
-  amproc => 'pg_lsn_hash' },
-
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index da25befefe..e510e28655 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,40 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
-{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
-  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
-  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
-{ opcmethod => 'brin', opcname => 'char_bloom_ops',
-  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
-  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
-{ opcmethod => 'brin', opcname => 'name_bloom_ops',
-  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
-  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
 { opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
   opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
-  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
-  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
 { opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
   opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
-  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
-  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
 { opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
   opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
-  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
-  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
-{ opcmethod => 'brin', opcname => 'text_bloom_ops',
-  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
-  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
 { opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
   opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
-  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
-  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
-{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
-  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
-  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
   opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
   opckeytype => 'tid', opcdefault => 'f' },
@@ -336,108 +309,72 @@
 { opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
   opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
-  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
-  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
 { opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
   opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
-  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
-  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
   opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
-  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
-  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
   opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
-  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
-  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
   opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
-  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
-  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
-{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
-  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
-  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
 { opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
   opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'time_bloom_ops',
-  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
-  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
 { opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
   opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'date_bloom_ops',
-  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
-  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
   opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
-  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
-  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
   opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
-  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
-  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
 { opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
   opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
-  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
-  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
   opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
-  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
-  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -449,9 +386,6 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
   opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
-  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
-  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
@@ -461,9 +395,6 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
   opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
-  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
-  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
@@ -473,9 +404,6 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
   opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
-  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
-  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 04edca6cb0..6218eade08 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -184,96 +184,62 @@
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
 { oid => '4602',
   opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
-{ oid => '4572',
-  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4603',
   opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
-{ oid => '4573',
-  opfmethod => 'brin', opfname => 'text_bloom_ops' },
-{ oid => '4574',
-  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
 { oid => '4604',
   opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
-{ oid => '4575',
-  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
 { oid => '4605',
   opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
-{ oid => '4576',
-  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
-{ oid => '4577',
-  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
-{ oid => '4578',
-  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
-{ oid => '4579',
-  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
 { oid => '4606',
   opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
-{ oid => '4580',
-  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
-{ oid => '4581',
-  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4607',
   opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
 { oid => '4608',
   opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
-{ oid => '4582',
-  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
 { oid => '4609',
   opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
-{ oid => '4583',
-  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
 { oid => '4610',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
-{ oid => '4584',
-  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4611',
   opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
-{ oid => '4585',
-  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
-{ oid => '4586',
-  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
 { oid => '4612',
   opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
-{ oid => '4587',
-  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
 { oid => '4613',
   opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
-{ oid => '4588',
-  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
@@ -282,16 +248,12 @@
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
 { oid => '4614',
   opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
-{ oid => '4589',
-  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
 { oid => '4615',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
-{ oid => '4590',
-  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 183358c1ba..cb7d421d5a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8309,26 +8309,6 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
-# BRIN bloom
-{ oid => '4591', descr => 'BRIN bloom support',
-  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
-{ oid => '4592', descr => 'BRIN bloom support',
-  proname => 'brin_bloom_add_value', prorettype => 'bool',
-  proargtypes => 'internal internal internal internal',
-  prosrc => 'brin_bloom_add_value' },
-{ oid => '4593', descr => 'BRIN bloom support',
-  proname => 'brin_bloom_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal int4',
-  prosrc => 'brin_bloom_consistent' },
-{ oid => '4594', descr => 'BRIN bloom support',
-  proname => 'brin_bloom_union', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
-  prosrc => 'brin_bloom_union' },
-{ oid => '4595', descr => 'BRIN bloom support',
-  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
-  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
-
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11502,20 +11482,6 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
-{ oid => '4596', descr => 'I/O',
-  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
-  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
-{ oid => '4597', descr => 'I/O',
-  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
-  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
-{ oid => '4598', descr => 'I/O',
-  proname => 'brin_bloom_summary_recv', provolatile => 's',
-  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
-  prosrc => 'brin_bloom_summary_recv' },
-{ oid => '4599', descr => 'I/O',
-  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
-  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
-
 { oid => '4638', descr => 'I/O',
   proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
   proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8c145c00be..387d25d66a 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,12 +679,6 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-{ oid => '4600',
-  descr => 'BRIN bloom summary',
-  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
-  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
-  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
-  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 { oid => '4601',
   descr => 'BRIN minmax-multi summary',
   typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index c6e49affeb..1c6bbfcf54 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom brin_multi
+test: brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 1d7eb418a7..dd3f3a9f0b 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,7 +108,6 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
-test: brin_bloom
 test: brin_multi
 test: gin
 test: gist
-- 
2.30.2

0008-move-minmax-multi-to-contrib-20210322.patchtext/x-patch; charset=UTF-8; name=0008-move-minmax-multi-to-contrib-20210322.patchDownload
From c2ce138638fdda69a747b3c55433a31063ef8602 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Mon, 22 Mar 2021 05:10:37 +0100
Subject: [PATCH 8/8] move minmax-multi to contrib

---
 contrib/Makefile                              |   1 +
 contrib/brin_minmax_multi/.gitignore          |   4 +
 contrib/brin_minmax_multi/Makefile            |  23 +
 .../brin_minmax_multi--1.0.sql                | 514 +++++++++++++++
 .../brin_minmax_multi}/brin_minmax_multi.c    |  61 +-
 .../brin_minmax_multi.control                 |   6 +
 .../expected/brin_multi.out                   |   0
 .../brin_minmax_multi}/sql/brin_multi.sql     |  27 +-
 src/backend/access/brin/Makefile              |   1 -
 src/include/catalog/pg_amop.dat               | 545 ----------------
 src/include/catalog/pg_amproc.dat             | 597 ------------------
 src/include/catalog/pg_opclass.dat            |  57 --
 src/include/catalog/pg_opfamily.dat           |  28 -
 src/include/catalog/pg_proc.dat               |  85 +--
 src/include/catalog/pg_type.dat               |   6 -
 15 files changed, 635 insertions(+), 1320 deletions(-)
 create mode 100644 contrib/brin_minmax_multi/.gitignore
 create mode 100644 contrib/brin_minmax_multi/Makefile
 create mode 100644 contrib/brin_minmax_multi/brin_minmax_multi--1.0.sql
 rename {src/backend/access/brin => contrib/brin_minmax_multi}/brin_minmax_multi.c (96%)
 create mode 100644 contrib/brin_minmax_multi/brin_minmax_multi.control
 rename {src/test/regress => contrib/brin_minmax_multi}/expected/brin_multi.out (100%)
 rename {src/test/regress => contrib/brin_minmax_multi}/sql/brin_multi.sql (96%)

diff --git a/contrib/Makefile b/contrib/Makefile
index f878287429..bd131f8fc3 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -13,6 +13,7 @@ SUBDIRS = \
 		btree_gin	\
 		btree_gist	\
 		brin_bloom	\
+		brin_minmax_multi	\
 		citext		\
 		cube		\
 		dblink		\
diff --git a/contrib/brin_minmax_multi/.gitignore b/contrib/brin_minmax_multi/.gitignore
new file mode 100644
index 0000000000..5dcb3ff972
--- /dev/null
+++ b/contrib/brin_minmax_multi/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/brin_minmax_multi/Makefile b/contrib/brin_minmax_multi/Makefile
new file mode 100644
index 0000000000..9ca5112339
--- /dev/null
+++ b/contrib/brin_minmax_multi/Makefile
@@ -0,0 +1,23 @@
+# contrib/brin_minmax_multi/Makefile
+
+MODULE_big = brin_minmax_multi
+OBJS = \
+	$(WIN32RES) \
+	brin_minmax_multi.o
+
+EXTENSION = brin_minmax_multi
+DATA = brin_minmax_multi--1.0.sql
+PGFILEDESC = "brin_minmax_multi - BRIN minmax-multi operator classes"
+
+REGRESS = brin_multi
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/brin_minmax_multi
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/brin_minmax_multi/brin_minmax_multi--1.0.sql b/contrib/brin_minmax_multi/brin_minmax_multi--1.0.sql
new file mode 100644
index 0000000000..98766215a0
--- /dev/null
+++ b/contrib/brin_minmax_multi/brin_minmax_multi--1.0.sql
@@ -0,0 +1,514 @@
+/* contrib/brin_minmax_multi/brin_minmax_multi--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION brin_minmax_multi" to load this file. \quit
+
+CREATE TYPE brin_minmax_multi_summary;
+
+-- BRIN bloom summary data type
+CREATE FUNCTION brin_minmax_multi_summary_in(cstring)
+RETURNS brin_minmax_multi_summary
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_minmax_multi_summary_out(brin_minmax_multi_summary)
+RETURNS cstring
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_minmax_multi_summary_recv(internal)
+RETURNS brin_minmax_multi_summary
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_minmax_multi_summary_send(brin_minmax_multi_summary)
+RETURNS bytea
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE TYPE brin_minmax_multi_summary (
+	INTERNALLENGTH = -1,
+	INPUT = brin_minmax_multi_summary_in,
+	OUTPUT = brin_minmax_multi_summary_out,
+	RECEIVE = brin_minmax_multi_summary_recv,
+	SEND = brin_minmax_multi_summary_send,
+	STORAGE = extended
+);
+
+-- BRIN support procedures
+CREATE FUNCTION brin_minmax_multi_opcinfo(internal)
+RETURNS internal
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_add_value(internal, internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_consistent(internal, internal, internal, int4)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_union(internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_options(internal)
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+-- distance functions
+
+CREATE FUNCTION brin_minmax_multi_distance_int2(int2, int2)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_int4(int4, int4)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_int8(int8, int8)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_float4(float4, float4)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_float8(float8, float8)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_numeric(numeric, numeric)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_tid(tid, tid)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_uuid(uuid, uuid)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_date(date, date)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_time(time, time)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_interval(interval, interval)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_timetz(timetz, timetz)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_pg_lsn(pg_lsn, pg_lsn)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_macaddr(macaddr, macaddr)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_macaddr8(macaddr8, macaddr8)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_inet(inet, inet)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_timestamp(timestamp, timestamp)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+
+CREATE OPERATOR CLASS date_minmax_multi_ops
+FOR TYPE date USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_date(date,date),
+STORAGE         date;
+
+CREATE OPERATOR CLASS float4_minmax_multi_ops
+FOR TYPE float4 USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+
+    OPERATOR        1       <(float4,float8),
+    OPERATOR        2       <=(float4,float8),
+    OPERATOR        3       =(float4,float8),
+    OPERATOR        4       >=(float4,float8),
+    OPERATOR        5       >(float4,float8),
+
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_float4(float4,float4),
+STORAGE         float4;
+
+CREATE OPERATOR CLASS float8_minmax_multi_ops
+FOR TYPE float8 USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+
+    OPERATOR        1       <(float8,float4),
+    OPERATOR        2       <=(float8,float4),
+    OPERATOR        3       =(float8,float4),
+    OPERATOR        4       >=(float8,float4),
+    OPERATOR        5       >(float8,float4),
+
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_float8(float8,float8),
+STORAGE         float8;
+
+CREATE OPERATOR CLASS inet_minmax_multi_ops
+FOR TYPE inet USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_inet(inet,inet),
+STORAGE         inet;
+
+CREATE OPERATOR CLASS int2_minmax_multi_ops
+FOR TYPE int2 USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+
+    OPERATOR        1       <(int2,int4),
+    OPERATOR        2       <=(int2,int4),
+    OPERATOR        3       =(int2,int4),
+    OPERATOR        4       >=(int2,int4),
+    OPERATOR        5       >(int2,int4),
+
+    OPERATOR        1       <(int2,int8),
+    OPERATOR        2       <=(int2,int8),
+    OPERATOR        3       =(int2,int8),
+    OPERATOR        4       >=(int2,int8),
+    OPERATOR        5       >(int2,int8),
+
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_int2(int2,int2),
+STORAGE         int2;
+
+CREATE OPERATOR CLASS int4_minmax_multi_ops
+FOR TYPE int4 USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+
+    OPERATOR        1       <(int4,int2),
+    OPERATOR        2       <=(int4,int2),
+    OPERATOR        3       =(int4,int2),
+    OPERATOR        4       >=(int4,int2),
+    OPERATOR        5       >(int4,int2),
+
+    OPERATOR        1       <(int4,int8),
+    OPERATOR        2       <=(int4,int8),
+    OPERATOR        3       =(int4,int8),
+    OPERATOR        4       >=(int4,int8),
+    OPERATOR        5       >(int4,int8),
+
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_int4(int4,int4),
+STORAGE         int4;
+
+CREATE OPERATOR CLASS int8_minmax_multi_ops
+FOR TYPE int8 USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+
+    OPERATOR        1       <(int8,int2),
+    OPERATOR        2       <=(int8,int2),
+    OPERATOR        3       =(int8,int2),
+    OPERATOR        4       >=(int8,int2),
+    OPERATOR        5       >(int8,int2),
+
+    OPERATOR        1       <(int8,int4),
+    OPERATOR        2       <=(int8,int4),
+    OPERATOR        3       =(int8,int4),
+    OPERATOR        4       >=(int8,int4),
+    OPERATOR        5       >(int8,int4),
+
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_int8(int8,int8),
+STORAGE         int8;
+
+CREATE OPERATOR CLASS interval_minmax_multi_ops
+FOR TYPE interval USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_interval(interval,interval),
+STORAGE         interval;
+
+CREATE OPERATOR CLASS macaddr_minmax_multi_ops
+FOR TYPE macaddr USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_macaddr(macaddr,macaddr),
+STORAGE         macaddr;
+
+CREATE OPERATOR CLASS macaddr8_minmax_multi_ops
+FOR TYPE macaddr8 USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_macaddr8(macaddr8,macaddr8),
+STORAGE         macaddr8;
+
+CREATE OPERATOR CLASS numeric_minmax_multi_ops
+FOR TYPE numeric USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_numeric(numeric,numeric),
+STORAGE         numeric;
+
+CREATE OPERATOR CLASS oid_minmax_multi_ops
+FOR TYPE oid USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_int4(int4,int4),
+STORAGE         oid;
+
+CREATE OPERATOR CLASS pg_lsn_minmax_multi_ops
+FOR TYPE pg_lsn USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_pg_lsn(pg_lsn,pg_lsn),
+STORAGE         pg_lsn;
+
+CREATE OPERATOR CLASS tid_minmax_multi_ops
+FOR TYPE tid USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_tid(tid,tid),
+STORAGE         tid;
+
+CREATE OPERATOR CLASS time_minmax_multi_ops
+FOR TYPE time USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_time(time,time),
+STORAGE         time;
+
+CREATE OPERATOR CLASS timestamp_minmax_multi_ops
+FOR TYPE timestamp USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+
+    OPERATOR        1       <(timestamp,timestamptz),
+    OPERATOR        2       <=(timestamp,timestamptz),
+    OPERATOR        3       =(timestamp,timestamptz),
+    OPERATOR        4       >=(timestamp,timestamptz),
+    OPERATOR        5       >(timestamp,timestamptz),
+
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_timestamp(timestamp,timestamp),
+STORAGE         timestamp;
+
+CREATE OPERATOR CLASS timestamptz_minmax_multi_ops
+FOR TYPE timestamptz USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_timestamp(timestamp,timestamp),
+STORAGE         timestamptz;
+
+CREATE OPERATOR CLASS timetz_minmax_multi_ops
+FOR TYPE timetz USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_timetz(timetz,timetz),
+STORAGE         timetz;
+
+CREATE OPERATOR CLASS uuid_minmax_multi_ops
+FOR TYPE uuid USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_uuid(uuid,uuid),
+STORAGE         uuid;
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/contrib/brin_minmax_multi/brin_minmax_multi.c
similarity index 96%
rename from src/backend/access/brin/brin_minmax_multi.c
rename to contrib/brin_minmax_multi/brin_minmax_multi.c
index 1ca86b7fb7..61629ce25c 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/contrib/brin_minmax_multi/brin_minmax_multi.c
@@ -83,6 +83,8 @@
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
 
+PG_MODULE_MAGIC;
+
 /*
  * Additional SQL level support functions
  *
@@ -257,6 +259,63 @@ static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
 													uint16 attno, Oid subtype,
 													uint16 strategynum);
 
+Datum brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_date(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_time(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_add_value(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_consistent(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_union(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_options(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_summary_in(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_summary_out(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_summary_send(PG_FUNCTION_ARGS);
+
+PG_FUNCTION_INFO_V1(brin_minmax_multi_summary_in);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_summary_out);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_summary_send);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_summary_recv);
+
+PG_FUNCTION_INFO_V1(brin_minmax_multi_opcinfo);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_options);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_add_value);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_consistent);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_union);
+
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_int2);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_int4);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_int8);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_float4);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_float8);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_numeric);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_tid);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_uuid);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_date);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_time);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_interval);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_timetz);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_pg_lsn);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_macaddr);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_macaddr8);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_inet);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_timestamp);
+
+
 typedef struct compare_context
 {
 	FmgrInfo   *cmpFn;
@@ -1794,7 +1853,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
-	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
 
 	PG_RETURN_POINTER(result);
 }
diff --git a/contrib/brin_minmax_multi/brin_minmax_multi.control b/contrib/brin_minmax_multi/brin_minmax_multi.control
new file mode 100644
index 0000000000..71e614867a
--- /dev/null
+++ b/contrib/brin_minmax_multi/brin_minmax_multi.control
@@ -0,0 +1,6 @@
+# brin_minmax_multi extension
+comment = 'support for BRIN minmax-multi indexes'
+default_version = '1.0'
+module_pathname = '$libdir/brin_minmax_multi'
+relocatable = true
+trusted = true
diff --git a/src/test/regress/expected/brin_multi.out b/contrib/brin_minmax_multi/expected/brin_multi.out
similarity index 100%
rename from src/test/regress/expected/brin_multi.out
rename to contrib/brin_minmax_multi/expected/brin_multi.out
diff --git a/src/test/regress/sql/brin_multi.sql b/contrib/brin_minmax_multi/sql/brin_multi.sql
similarity index 96%
rename from src/test/regress/sql/brin_multi.sql
rename to contrib/brin_minmax_multi/sql/brin_multi.sql
index f5ffcaea33..49f5bb36b5 100644
--- a/src/test/regress/sql/brin_multi.sql
+++ b/contrib/brin_minmax_multi/sql/brin_multi.sql
@@ -1,3 +1,28 @@
+CREATE EXTENSION brin_minmax_multi;
+
+CREATE TABLE tenk1 (
+	unique1		int4,
+	unique2		int4,
+	two			int4,
+	four		int4,
+	ten			int4,
+	twenty		int4,
+	hundred		int4,
+	thousand	int4,
+	twothousand	int4,
+	fivethous	int4,
+	tenthous	int4,
+	odd			int4,
+	even		int4,
+	stringu1	name,
+	stringu2	name,
+	string4		name
+);
+
+\copy tenk1 from '../../src/test/regress/data/tenk.data'
+
+CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops);
+
 CREATE TABLE brintest_multi (
 	int8col bigint,
 	int2col smallint,
@@ -94,7 +119,7 @@ CREATE INDEX brinidx_multi ON brintest_multi USING brin (
 	float8col float8_minmax_multi_ops,
 	macaddrcol macaddr_minmax_multi_ops,
 	inetcol inet_minmax_multi_ops,
-	cidrcol inet_minmax_multi_ops,
+--	cidrcol inet_minmax_multi_ops,
 	datecol date_minmax_multi_ops,
 	timecol time_minmax_multi_ops,
 	timestampcol timestamp_minmax_multi_ops,
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 75eb87ec10..468e1e289a 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -16,7 +16,6 @@ OBJS = \
 	brin.o \
 	brin_inclusion.o \
 	brin_minmax.o \
-	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 51bef55a57..0f7ff63669 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1994,152 +1994,6 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
-# minmax multi integer
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
-  amopmethod => 'brin' },
-
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2174,23 +2028,6 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
-# minmax multi oid
-{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
-  amopmethod => 'brin' },
-
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2208,23 +2045,6 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
-# minmax multi tid
-{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
-  amopmethod => 'brin' },
-
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2291,72 +2111,6 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
-# minmax multi float (float4, float8)
-
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '2',
-  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '4',
-  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float8', amopstrategy => '2',
-  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float8', amopstrategy => '4',
-  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float4', amopstrategy => '2',
-  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float4', amopstrategy => '4',
-  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '2',
-  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '4',
-  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
-  amopmethod => 'brin' },
-
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2374,23 +2128,6 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
-# minmax multi macaddr
-{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '1',
-  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '2',
-  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '3',
-  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '4',
-  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '5',
-  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
-
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2408,23 +2145,6 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
-# minmax multi macaddr8
-{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '1',
-  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '2',
-  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '3',
-  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '4',
-  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '5',
-  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
-
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2442,23 +2162,6 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
-# minmax multi inet
-{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
-  amopmethod => 'brin' },
-
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2513,23 +2216,6 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
-# minmax multi time without time zone
-{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
-  amopmethod => 'brin' },
-
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2676,152 +2362,6 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
-# minmax multi datetime (date, timestamp, timestamptz)
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '1',
-  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '2',
-  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '3',
-  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '4',
-  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '5',
-  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamptz', amopstrategy => '1',
-  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamptz', amopstrategy => '2',
-  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamptz', amopstrategy => '3',
-  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamptz', amopstrategy => '4',
-  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamptz', amopstrategy => '5',
-  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamp', amopstrategy => '1',
-  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamp', amopstrategy => '2',
-  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamp', amopstrategy => '3',
-  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamp', amopstrategy => '4',
-  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamp', amopstrategy => '5',
-  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamptz', amopstrategy => '1',
-  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamptz', amopstrategy => '2',
-  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamptz', amopstrategy => '3',
-  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamptz', amopstrategy => '4',
-  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamptz', amopstrategy => '5',
-  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'date', amopstrategy => '1',
-  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'date', amopstrategy => '2',
-  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'date', amopstrategy => '3',
-  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'date', amopstrategy => '4',
-  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'date', amopstrategy => '5',
-  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamp', amopstrategy => '1',
-  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamp', amopstrategy => '2',
-  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamp', amopstrategy => '3',
-  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamp', amopstrategy => '4',
-  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamp', amopstrategy => '5',
-  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '1',
-  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '2',
-  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '3',
-  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '4',
-  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '5',
-  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
-
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2839,23 +2379,6 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
-# minmax multi interval
-{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '1',
-  amopopr => '<(interval,interval)', amopmethod => 'brin' },
-{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '2',
-  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
-{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '3',
-  amopopr => '=(interval,interval)', amopmethod => 'brin' },
-{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '4',
-  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
-{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '5',
-  amopopr => '>(interval,interval)', amopmethod => 'brin' },
-
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2873,23 +2396,6 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
-# minmax multi time with time zone
-{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '2',
-  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '4',
-  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
-  amopmethod => 'brin' },
-
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2941,23 +2447,6 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
-# minmax multi numeric
-{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '1',
-  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
-{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '2',
-  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
-{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '3',
-  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
-{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '4',
-  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
-{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '5',
-  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
-
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2975,23 +2464,6 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
-# minmax multi uuid
-{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
-  amopmethod => 'brin' },
-
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -3053,23 +2525,6 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
-# minmax multi pg_lsn
-{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '2',
-  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
-{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '4',
-  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
-{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
-  amopmethod => 'brin' },
-
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 68c3342eeb..9192a66a88 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -932,152 +932,6 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# minmax multi integer: int2, int4, int8
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
-
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -1103,23 +957,6 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# minmax multi oid
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
-
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1132,23 +969,6 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# minmax multi tid
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
-
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1199,80 +1019,6 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi float
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_float4' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_float8' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_float8' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_float8' },
-
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1287,26 +1033,6 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi macaddr
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_macaddr' },
-
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1321,26 +1047,6 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi macaddr8
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_macaddr8' },
-
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1354,26 +1060,6 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# minmax multi inet
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_inet' },
-
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1421,25 +1107,6 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# minmax multi time without time zone
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_time' },
-
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1547,170 +1214,6 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi datetime (date, timestamp, timestamptz)
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_time' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_time' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_time' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_time' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_timestamp' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_timestamp' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_date' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_date' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_date' },
-
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1725,26 +1228,6 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi interval
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_interval' },
-
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1759,26 +1242,6 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi time with time zone
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_timetz' },
-
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1819,26 +1282,6 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi numeric
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_numeric' },
-
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1853,26 +1296,6 @@
   amprocrighttype => 'uuid', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi uuid
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_uuid' },
-
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1910,26 +1333,6 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi pg_lsn
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_pg_lsn' },
-
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index e510e28655..24b1433e1f 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,64 +275,34 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
-{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
-  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
-  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
-{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
-  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
-  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
-{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
-  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
-  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
-{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
-  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
-  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
-{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
-  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
-  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
-{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
-  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
-  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
-{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
-  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
-  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
-{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
-  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
-  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
-{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
-  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
-  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
-{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
-  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
-  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
@@ -342,39 +312,21 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
-{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
-  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
-  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
-{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
-  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
-  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
-{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
-  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
-  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
-{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
-  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
-  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
-{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
-  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
-  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
-{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
-  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
-  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -383,27 +335,18 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
-{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
-  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
-  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
-{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
-  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
-  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
-{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
-  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
-  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 6218eade08..57e5aa0d8b 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,22 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
-{ oid => '4602',
-  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
-{ oid => '4603',
-  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
-{ oid => '4604',
-  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
-{ oid => '4605',
-  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
 { oid => '4064',
@@ -206,54 +198,34 @@
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
-{ oid => '4606',
-  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
-{ oid => '4607',
-  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
-{ oid => '4608',
-  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
-{ oid => '4609',
-  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
-{ oid => '4610',
-  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
-{ oid => '4611',
-  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
-{ oid => '4612',
-  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
-{ oid => '4613',
-  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
-{ oid => '4614',
-  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
-{ oid => '4615',
-  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cb7d421d5a..ca240cef1a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8221,76 +8221,7 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
-# BRIN minmax multi
-{ oid => '4616', descr => 'BRIN multi minmax support',
-  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
-{ oid => '4617', descr => 'BRIN multi minmax support',
-  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
-  proargtypes => 'internal internal internal internal',
-  prosrc => 'brin_minmax_multi_add_value' },
-{ oid => '4618', descr => 'BRIN multi minmax support',
-  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal int4',
-  prosrc => 'brin_minmax_multi_consistent' },
-{ oid => '4619', descr => 'BRIN multi minmax support',
-  proname => 'brin_minmax_multi_union', prorettype => 'bool',
-  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
-{ oid => '4620', descr => 'BRIN multi minmax support',
-  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
-  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
-
-{ oid => '4621', descr => 'BRIN multi minmax int2 distance',
-  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
-{ oid => '4622', descr => 'BRIN multi minmax int4 distance',
-  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
-{ oid => '4623', descr => 'BRIN multi minmax int8 distance',
-  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
-{ oid => '4624', descr => 'BRIN multi minmax float4 distance',
-  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
-{ oid => '4625', descr => 'BRIN multi minmax float8 distance',
-  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
-{ oid => '4626', descr => 'BRIN multi minmax numeric distance',
-  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
-{ oid => '4627', descr => 'BRIN multi minmax tid distance',
-  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
-{ oid => '4628', descr => 'BRIN multi minmax uuid distance',
-  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
-{ oid => '4629', descr => 'BRIN multi minmax date distance',
-  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
-{ oid => '4630', descr => 'BRIN multi minmax time distance',
-  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
-{ oid => '4631', descr => 'BRIN multi minmax interval distance',
-  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
-{ oid => '4632', descr => 'BRIN multi minmax timetz distance',
-  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
-{ oid => '4633', descr => 'BRIN multi minmax pg_lsn distance',
-  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
-{ oid => '4634', descr => 'BRIN multi minmax macaddr distance',
-  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
-{ oid => '4635', descr => 'BRIN multi minmax macaddr8 distance',
-  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
-{ oid => '4636', descr => 'BRIN multi minmax inet distance',
-  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
-{ oid => '4637', descr => 'BRIN multi minmax timestamp distance',
-  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
@@ -11482,18 +11413,4 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
-{ oid => '4638', descr => 'I/O',
-  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
-  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
-{ oid => '4639', descr => 'I/O',
-  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
-  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
-{ oid => '4640', descr => 'I/O',
-  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
-  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
-  prosrc => 'brin_minmax_multi_summary_recv' },
-{ oid => '4641', descr => 'I/O',
-  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
-  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
-
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 387d25d66a..920dbbbfeb 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,10 +679,4 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-{ oid => '4601',
-  descr => 'BRIN minmax-multi summary',
-  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
-  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
-  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
-  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
-- 
2.30.2

#165Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#164)
Re: WIP: BRIN multi-range indexes

On 3/22/21 6:27 AM, Tomas Vondra wrote:

...

All the regression tests work fine, with the exception of minmax-multi
on a CIDR column. I don't know why, but the CREATE INDEX then fails like
this:

ERROR: missing operator 1(650,650) in opfamily 16463

650 is cidr, so this essentially says there's no (cidr<cidr) operator.
With the opclasses defined in .dat files this however worked, so I
suppose it's related to the missing operator families.

Turns out this is likely a bug elsewhere. After a couple fixes to the
extension SQL script, the only real difference in catalog contents
(compared to e.g. the built-in BRIN minmax inet opclass) is that the
built-in one has opckeytype set to 869 (i.e. inet), while the one
created from extension has it set to 0.

The opclasses for inet (OID 869) look like this:

test=# select oid, opcname, opcfamily, opcintype, opckeytype from
pg_opclass where opcintype = 869 order by oid;
oid | opcname | opcfamily | opcintype | opckeytype
-------+-----------------------+-----------+-----------+------------
10009 | cidr_ops | 1974 | 869 | 0
10010 | cidr_ops | 1975 | 869 | 0
10015 | inet_ops | 1974 | 869 | 0
10016 | inet_ops | 1975 | 869 | 0
10017 | inet_ops | 3550 | 869 | 0
10018 | inet_ops | 3794 | 869 | 0
10105 | inet_minmax_ops | 4075 | 869 | 869
10106 | inet_inclusion_ops | 4102 | 869 | 869
16451 | inet_bloom_ops | 16450 | 869 | 0
17398 | inet_minmax_multi_ops | 17397 | 869 | 0
(10 rows)

The only difference between the two minmax variants is the opckeytype,
and if I update it to 869 for inet_minmax_multi_ops, it starts working.
Likewise, if I set it to 0 for inet_minmax_ops, it breaks the same way.

Turns out, this is impossible to set from CREATE OPERATOR CLASS, because
opclasscmds.c does this:

/* Just drop the spec if same as column datatype */
if (storageoid == typeoid && false)
storageoid = InvalidOid;

but index.c looks only at opclassTup->opckeytype. The built-in opclasses
don't have this issue, because they put the data into the catalog
directly, without going through this code.

I don't know what's the right fix, but it seems like this patch has
nothing to do with it. If we want to move the opclasses into an
extension, we can comment out that one (cidr/inet) case for now.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#166Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#165)
5 attachment(s)
Re: WIP: BRIN multi-range indexes

I've pushed the first couple patches, reworking the BRIN interface and
the two existing opclasses. Attached are are the remaining bits,
implementing the two new opclasses.

0001 fixes the opckeytype I explained in the previous message. I'll
start a new thread, so that it does not get buried in this thread.

The 0002 and 0003 are the "main" patches, implementing all the stuff as
in-core opclasses. These patches were reviewed multiple times before, no
new changes.

0004 and 0005 are the patches moving the new opclasses to contrib. This
should also undo the catversion and OID generation changes from 0002 and
0003, but I'll take care of that if we actually decide contrib is the
right way. I kinda like it, if we can solve the two issues:

1) the opckeytype - I think this is a bug elsewhere, affecting any
opclass created by CREATE OPERATOR CLASS and not by directly injecting
the data into catalogs. I'll start a separate thread for that.

2) the pageinspect - Without knowing OID of the types used for summary,
brin_page_items can't print info about the bloom filter, minmax-multi
ranges, etc. Unfortunately, by moving the code to contrib we lose the
static OID assignment. I think there are three solutions to this:

a) Just use BYTEAOID, and accept that pageinspect prints just and
incomprehensible stream of characters

b) Use TypenameGetTypidExtended() to lookup the type by name. This is
what the code does now, but I'm afraid this might have security issues
due to search_path shenanigans. Base types can be created only by
superusers, but it's possible to create a composite type, and confuse
the lookup ...

c) Don't bother passing OID, and instead pass a pointer to the output
function directly. We'd need to extend BrinOpcInfo a bit, but that seems
like the simplest solution.

I think (b) is too dangerous/fragile, (a) is the simplest and (c) is a
bit too invasive/ugly I think.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

0001-fixup-opclass-storage-type-20210323.patchtext/x-patch; charset=UTF-8; name=0001-fixup-opclass-storage-type-20210323.patchDownload
From 4ee5e2bbd0c44c82d1604836db5352f3e0e0fbf2 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Mon, 22 Mar 2021 20:36:59 +0100
Subject: [PATCH 1/5] fixup opclass storage type

---
 src/backend/commands/opclasscmds.c | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index fad39e2b75..78b2d69782 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -576,10 +576,7 @@ DefineOpClass(CreateOpClassStmt *stmt)
 	 */
 	if (OidIsValid(storageoid))
 	{
-		/* Just drop the spec if same as column datatype */
-		if (storageoid == typeoid)
-			storageoid = InvalidOid;
-		else if (!amstorage)
+		if ((storageoid != typeoid) && (!amstorage))
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 					 errmsg("storage type cannot be different from data type for access method \"%s\"",
-- 
2.30.2

0002-BRIN-bloom-indexes-20210323.patchtext/x-patch; charset=UTF-8; name=0002-BRIN-bloom-indexes-20210323.patchDownload
From 4dc43bac76cbcf8d9f4015faa6c56f607aaf63bb Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Tue, 23 Mar 2021 01:02:55 +0100
Subject: [PATCH 2/5] BRIN bloom indexes

Adds a BRIN opclass using a Bloom filter to summarize the range. BRIN
indexes using the new opclasses only allow equality queries, but that
works for data like UUID, MAC addresses etc. for which range queries are
not very common (and the data look random, making BRIN minmax indexes
inefficient anyway).

BRIN bloom opclasses allow specifying the usual Bloom parameters, like
expected number of distinct values (in the BRIN range) and desired false
positive rate.

The opclasses do not operate directly on the indexed values, but on
32-bit hashes of the values. That assumes the hash functions for data
types have a low number of collisions, good performance etc. Collisions
should not be a huge issue though, because the number of values in a
BRIN ranges is usually fairly small.

Bump catversion, due to various catalog changes.

Author: Tomas Vondra <tomas.vondra@postgresql.org>
Reviewed-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Reviewed-by: Nico Williams <nico@cryptonector.com>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                    | 226 +++++-
 src/backend/access/brin/Makefile          |   1 +
 src/backend/access/brin/brin_bloom.c      | 809 ++++++++++++++++++++++
 src/include/catalog/catversion.h          |   2 +-
 src/include/catalog/pg_amop.dat           | 116 ++++
 src/include/catalog/pg_amproc.dat         | 447 ++++++++++++
 src/include/catalog/pg_opclass.dat        |  72 ++
 src/include/catalog/pg_opfamily.dat       |  38 +
 src/include/catalog/pg_proc.dat           |  34 +
 src/include/catalog/pg_type.dat           |   7 +-
 src/test/regress/expected/brin_bloom.out  | 428 ++++++++++++
 src/test/regress/expected/opr_sanity.out  |   3 +-
 src/test/regress/expected/psql.out        |   3 +-
 src/test/regress/expected/type_sanity.out |   7 +-
 src/test/regress/parallel_schedule        |   5 +
 src/test/regress/serial_schedule          |   1 +
 src/test/regress/sql/brin_bloom.sql       | 376 ++++++++++
 17 files changed, 2567 insertions(+), 8 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 078f51bb55..cb4f9b08b9 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -115,7 +115,8 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
   operator classes store the minimum and the maximum values appearing
   in the indexed column within the range.  The <firstterm>inclusion</firstterm>
   operator classes store a value which includes the values in the indexed
-  column within the range.
+  column within the range.  The <firstterm>bloom</firstterm> operator
+  classes build a Bloom filter for all values in the range.
  </para>
 
  <table id="brin-builtin-opclasses-table">
@@ -154,6 +155,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>|&amp;&gt; (box,box)</literal></entry></row>
     <row><entry><literal>|&gt;&gt; (box,box)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bpchar_bloom_ops</literal></entry>
+     <entry><literal>= (character,character)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bpchar_minmax_ops</literal></entry>
      <entry><literal>= (character,character)</literal></entry>
@@ -163,6 +169,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (character,character)</literal></entry></row>
     <row><entry><literal>&gt;= (character,character)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>bytea_bloom_ops</literal></entry>
+     <entry><literal>= (bytea,bytea)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>bytea_minmax_ops</literal></entry>
      <entry><literal>= (bytea,bytea)</literal></entry>
@@ -172,6 +183,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (bytea,bytea)</literal></entry></row>
     <row><entry><literal>&gt;= (bytea,bytea)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>char_bloom_ops</literal></entry>
+     <entry><literal>= ("char","char")</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>char_minmax_ops</literal></entry>
      <entry><literal>= ("char","char")</literal></entry>
@@ -181,6 +197,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; ("char","char")</literal></entry></row>
     <row><entry><literal>&gt;= ("char","char")</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>date_bloom_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>date_minmax_ops</literal></entry>
      <entry><literal>= (date,date)</literal></entry>
@@ -190,6 +211,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float4_minmax_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -199,6 +225,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>float8_minmax_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -218,6 +249,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>= (inet,inet)</literal></entry></row>
     <row><entry><literal>&amp;&amp; (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>inet_bloom_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>inet_minmax_ops</literal></entry>
      <entry><literal>= (inet,inet)</literal></entry>
@@ -227,6 +263,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int2_minmax_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -236,6 +277,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int4_minmax_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -245,6 +291,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>int8_minmax_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -254,6 +305,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>interval_minmax_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -263,6 +319,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr_minmax_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -272,6 +333,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>macaddr8_minmax_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -281,6 +347,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>name_bloom_ops</literal></entry>
+     <entry><literal>= (name,name)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>name_minmax_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -290,6 +361,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (name,name)</literal></entry></row>
     <row><entry><literal>&gt;= (name,name)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>numeric_bloom_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>numeric_minmax_ops</literal></entry>
      <entry><literal>= (numeric,numeric)</literal></entry>
@@ -299,6 +375,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>oid_minmax_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -308,6 +389,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -335,6 +421,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&amp;&gt; (anyrange,anyrange)</literal></entry></row>
     <row><entry><literal>-|- (anyrange,anyrange)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>text_bloom_ops</literal></entry>
+     <entry><literal>= (text,text)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>text_minmax_ops</literal></entry>
      <entry><literal>= (text,text)</literal></entry>
@@ -344,6 +435,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (text,text)</literal></entry></row>
     <row><entry><literal>&gt;= (text,text)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>tid_bloom_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>tid_minmax_ops</literal></entry>
      <entry><literal>= (tid,tid)</literal></entry>
@@ -353,6 +449,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamp_minmax_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -362,6 +463,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timestamptz_minmax_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -371,6 +477,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>time_bloom_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>time_minmax_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -380,6 +491,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>timetz_minmax_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -389,6 +505,11 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>uuid_minmax_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -409,6 +530,55 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    </tbody>
   </tgroup>
  </table>
+
+  <sect2 id="brin-builtin-opclasses--parameters">
+   <title>Operator Class Parameters</title>
+
+   <para>
+    Some of the built-in operator classes allow specifying parameters affecting
+    behavior of the operator class.  Each operator class has its own set of
+    allowed parameters.  Only the <literal>bloom</literal> operator class
+    allows specifying parameters:
+   </para>
+
+   <para>
+    <acronym>bloom</acronym> operator classes accept these parameters:
+   </para>
+
+   <variablelist>
+   <varlistentry>
+    <term><literal>n_distinct_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the estimated number of distinct non-null values in the block
+     range, used by <acronym>BRIN</acronym> bloom indexes for sizing of the
+     Bloom filter. It behaves similarly to <literal>n_distinct</literal> option
+     for <xref linkend="sql-altertable"/>. When set to a positive value,
+     each block range is assumed to contain this number of distinct non-null
+     values. When set to a negative value, which must be greater than or
+     equal to -1, the number of distinct non-null is assumed linear with
+     the maximum possible number of tuples in the block range (about 290
+     rows per block). The default value is <literal>-0.1</literal>, and
+     the minimum number of distinct non-null values is <literal>16</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>false_positive_rate</literal></term>
+    <listitem>
+    <para>
+     Defines the desired false positive rate used by <acronym>BRIN</acronym>
+     bloom indexes for sizing of the Bloom filter. The values must be
+     between 0.0001 and 0.25. The default value is 0.01, which is 1% false
+     positive rate.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   </variablelist>
+  </sect2>
+
 </sect1>
 
 <sect1 id="brin-extensibility">
@@ -781,6 +951,60 @@ typedef struct BrinOpcInfo
     function can improve index performance.
  </para>
 
+ <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 468e1e289a..6b56131215 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
+	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_pageops.o \
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000000..2214fb4d0c
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,809 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * A BRIN opclass summarizing page range into a bloom filter.
+ *
+ * Bloom filters allow efficient testing whether a given page range contains
+ * a particular value. Therefore, if we summarize each page range into a small
+ * bloom filter, we can easily (and cheaply) test whether it contains values
+ * we get later.
+ *
+ * The index only supports equality operators, similarly to hash indexes.
+ * Bloom indexes are however much smaller, and support only bitmap scans.
+ *
+ * Note: Don't confuse this with bloom indexes, implemented in a contrib
+ * module. That extension implements an entirely new AM, building a bloom
+ * filter on multiple columns in a single row. This opclass works with an
+ * existing AM (BRIN) and builds bloom filter on a column.
+ *
+ *
+ * values vs. hashes
+ * -----------------
+ *
+ * The original column values are not used directly, but are first hashed
+ * using the regular type-specific hash function, producing a uint32 hash.
+ * And this hash value is then added to the summary - i.e. it's hashed
+ * again and added to the bloom filter.
+ *
+ * This allows the code to treat all data types (byval/byref/...) the same
+ * way, with only minimal space requirements, because we're working with
+ * hashes and not the original values. Everything is uint32.
+ *
+ * Of course, this assumes the built-in hash function is reasonably good,
+ * without too many collisions etc. But that does seem to be the case, at
+ * least based on past experience. After all, the same hash functions are
+ * used for hash indexes, hash partitioning and so on.
+ *
+ *
+ * hashing scheme
+ * --------------
+ *
+ * Bloom filters require a number of independent hash functions. There are
+ * different schemes how to construct them - for example we might use
+ * hash_uint32_extended with random seeds, but that seems fairly expensive.
+ * We use a scheme requiring only two functions described in this paper:
+ *
+ * Less Hashing, Same Performance:Building a Better Bloom Filter
+ * Adam Kirsch, Michael Mitzenmacher†, Harvard School of Engineering and
+ * Applied Sciences, Cambridge, Massachusetts [DOI 10.1002/rsa.20208]
+ *
+ * The two hash functions h1 and h2 are calculated using hard-coded seeds,
+ * and then combined using (h1 + i * h2) to generate the hash functions.
+ *
+ *
+ * sizing the bloom filter
+ * -----------------------
+ *
+ * Size of a bloom filter depends on the number of distinct values we will
+ * store in it, and the desired false positive rate. The higher the number
+ * of distinct values and/or the lower the false positive rate, the larger
+ * the bloom filter. On the other hand, we want to keep the index as small
+ * as possible - that's one of the basic advantages of BRIN indexes.
+ *
+ * Although the number of distinct elements (in a page range) depends on
+ * the data, we can consider it fixed. This simplifies the trade-off to
+ * just false positive rate vs. size.
+ *
+ * At the page range level, false positive rate is a probability the bloom
+ * filter matches a random value. For the whole index (with sufficiently
+ * many page ranges) it represents the fraction of the index ranges (and
+ * thus fraction of the table to be scanned) matching the random value.
+ *
+ * Furthermore, the size of the bloom filter is subject to implementation
+ * limits - it has to fit onto a single index page (8kB by default). As
+ * the bitmap is inherently random (when "full" about half the bits is set
+ * to 1, randomly), compression can't help very much.
+ *
+ * To reduce the size of a filter (to fit to a page), we have to either
+ * accept higher false positive rate (undesirable), or reduce the number
+ * of distinct items to be stored in the filter. We can't alter the input
+ * data, of course, but we may make the BRIN page ranges smaller - instead
+ * of the default 128 pages (1MB) we may build index with 16-page ranges,
+ * or something like that. This should reduce the number of distinct values
+ * in the page range, making the filter smaller (with fixed false positive
+ * rate). Even for random data sets this should help, as the number of rows
+ * per heap page is limited (to ~290 with very narrow tables, likely ~20
+ * in practice).
+ *
+ * Of course, good sizing decisions depend on having the necessary data,
+ * i.e. number of distinct values in a page range (of a given size) and
+ * table size (to estimate cost change due to change in false positive
+ * rate due to having larger index vs. scanning larger indexes). We may
+ * not have that data - for example when building an index on empty table
+ * it's not really possible. And for some data we only have estimates for
+ * the whole table and we can only estimate per-range values (ndistinct).
+ *
+ * Another challenge is that while the bloom filter is per-column, it's
+ * the whole index tuple that has to fit into a page. And for multi-column
+ * indexes that may include pieces we have no control over (not necessarily
+ * bloom filters, the other columns may use other BRIN opclasses). So it's
+ * not entirely clear how to distribute the space between those columns.
+ *
+ * The current logic, implemented in brin_bloom_get_ndistinct, attempts to
+ * make some basic sizing decisions, based on the size of BRIN ranges, and
+ * the maximum number of rows per range.
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_page.h"
+#include "access/brin_tuple.h"
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions. We only have one, which is
+ * used to calculate hash of the input value.
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Storage type for BRIN's reloptions.
+ */
+typedef struct BloomOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	double		nDistinctPerRange;	/* number of distinct values per range */
+	double		falsePositiveRate;	/* false positive for bloom filter */
+} BloomOptions;
+
+/*
+ * The current min value (16) is somewhat arbitrary, but it's based
+ * on the fact that the filter header is ~20B alone, which is about
+ * the same as the filter bitmap for 16 distinct items with 1% false
+ * positive rate. So by allowing lower values we'd not gain much. In
+ * any case, the min should not be larger than MaxHeapTuplesPerPage
+ * (~290), which is the theoretical maximum for single-page ranges.
+ */
+#define		BLOOM_MIN_NDISTINCT_PER_RANGE		16
+
+/*
+ * Used to determine number of distinct items, based on the number of rows
+ * in a page range. The 10% is somewhat similar to what estimate_num_groups
+ * does, so we use the same factor here.
+ */
+#define		BLOOM_DEFAULT_NDISTINCT_PER_RANGE	-0.1	/* 10% of values */
+
+/*
+ * Allowed range and default value for the false positive range. The exact
+ * values are somewhat arbitrary, but were chosen considering the various
+ * parameters (size of filter vs. page size, etc.).
+ *
+ * The lower the false-positive rate, the more accurate the filter is, but
+ * it also gets larger - at some point this eliminates the main advantage
+ * of BRIN indexes, which is the tiny size. At 0.01% the index is about
+ * 10% of the table (assuming 290 distinct values per 8kB page).
+ *
+ * On the other hand, as the false-positive rate increases, larger part of
+ * the table has to be scanned due to mismatches - at 25% we're probably
+ * close to sequential scan being cheaper.
+ */
+#define		BLOOM_MIN_FALSE_POSITIVE_RATE	0.0001	/* 0.01% fp rate */
+#define		BLOOM_MAX_FALSE_POSITIVE_RATE	0.25	/* 25% fp rate */
+#define		BLOOM_DEFAULT_FALSE_POSITIVE_RATE	0.01	/* 1% fp rate */
+
+#define BloomGetNDistinctPerRange(opts) \
+	((opts) && (((BloomOptions *) (opts))->nDistinctPerRange != 0) ? \
+	 (((BloomOptions *) (opts))->nDistinctPerRange) : \
+	 BLOOM_DEFAULT_NDISTINCT_PER_RANGE)
+
+#define BloomGetFalsePositiveRate(opts) \
+	((opts) && (((BloomOptions *) (opts))->falsePositiveRate != 0.0) ? \
+	 (((BloomOptions *) (opts))->falsePositiveRate) : \
+	 BLOOM_DEFAULT_FALSE_POSITIVE_RATE)
+
+/*
+ * And estimate of the largest bloom we can fit onto a page. This is not
+ * a perfect guarantee, for a couple of reasons. For example, the row may
+ * be larger because the index has multiple columns.
+ */
+#define BloomMaxFilterSize \
+	MAXALIGN_DOWN(BLCKSZ - \
+				  (MAXALIGN(SizeOfPageHeaderData + \
+							sizeof(ItemIdData)) + \
+				   MAXALIGN(sizeof(BrinSpecialSpace)) + \
+				   SizeOfBrinTuple))
+
+/*
+ * Seeds used to calculate two hash functions h1 and h2, which are then used
+ * to generate k hashes using the (h1 + i * h2) scheme.
+ */
+#define BLOOM_SEED_1	0x71d924af
+#define BLOOM_SEED_2	0xba48b314
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * XXX We could implement "sparse" bloom filters, keeping only the bytes that
+ * are not entirely 0. But while indexes don't support TOAST, the varlena can
+ * still be compressed. So this seems unnecessary, because the compression
+ * should do the same job.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and then
+ * stop using it (and not store the bitmap, to save space) when the false
+ * positive rate gets too high. But even if the false positive rate exceeds the
+ * desired value, it still can eliminate some page ranges.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* space for various flags (unused for now) */
+	uint16		flags;
+
+	/* fields for the HASHED phase */
+	uint8		nhashes;		/* number of hash functions */
+	uint32		nbits;			/* number of bits in the bitmap (size) */
+	uint32		nbits_set;		/* number of bits set to 1 */
+
+	/* data of the bloom filter */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+
+}			BloomFilter;
+
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected values
+ * and the requested false positive rate. The filter is stored as varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size		len;
+	BloomFilter *filter;
+
+	int			nbits;			/* size of filter / number of bits */
+	int			nbytes;			/* size of filter / number of bytes */
+
+	double		k;				/* number of hash functions */
+
+	Assert(ndistinct > 0);
+	Assert((false_positive_rate >= BLOOM_MIN_FALSE_POSITIVE_RATE) &&
+		   (false_positive_rate < BLOOM_MAX_FALSE_POSITIVE_RATE));
+
+	/* sizing bloom filter: -(n * ln(p)) / (ln(2))^2 */
+	nbits = ceil(-(ndistinct * log(false_positive_rate)) / pow(log(2.0), 2));
+
+	/* round m to whole bytes */
+	nbytes = ((nbits + 7) / 8);
+	nbits = nbytes * 8;
+
+	/*
+	 * Reject filters that are obviously too large to store on a page.
+	 *
+	 * Initially the bloom filter is just zeroes and so very compressible, but
+	 * as we add values it gets more and more random, and so less and less
+	 * compressible. So initially everything fits on the page, but we might
+	 * get surprising failures later - we want to prevent that, so we reject
+	 * bloom filter that are obviously too large.
+	 *
+	 * XXX It's not uncommon to oversize the bloom filter a bit, to defend
+	 * against unexpected data anomalies (parts of table with more distinct
+	 * values per range etc.). But we still need to make sure even the
+	 * oversized filter fits on page, if such need arises.
+	 *
+	 * XXX This check is not perfect, because the index may have multiple
+	 * filters that are small individually, but too large when combined.
+	 */
+	if (nbytes > BloomMaxFilterSize)
+		elog(ERROR, "the bloom filter is too large (%d > %zu)", nbytes,
+			 BloomMaxFilterSize);
+
+	/*
+	 * round(log(2.0) * m / ndistinct), but assume round() may not be
+	 * available on Windows
+	 */
+	k = log(2.0) * nbits / ndistinct;
+	k = (k - floor(k) >= 0.5) ? ceil(k) : floor(k);
+
+	/*
+	 * We allocate the whole filter. Most of it is going to be 0 bits, so the
+	 * varlena is easy to compress.
+	 */
+	len = offsetof(BloomFilter, data) + nbytes;
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->flags = 0;
+	filter->nhashes = (int) k;
+	filter->nbits = nbits;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter * filter, uint32 value, bool *updated)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* compute the hashes, used for the bloom filter */
+	h1 = hash_bytes_uint32_extended(value, BLOOM_SEED_1) % filter->nbits;
+	h2 = hash_bytes_uint32_extended(value, BLOOM_SEED_2) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (!(filter->data[byte] & (0x01 << bit)))
+		{
+			filter->data[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+	}
+
+	return filter;
+}
+
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter * filter, uint32 value)
+{
+	int			i;
+	uint64		h1,
+				h2;
+
+	/* calculate the two hashes */
+	h1 = hash_bytes_uint32_extended(value, BLOOM_SEED_1) % filter->nbits;
+	h2 = hash_bytes_uint32_extended(value, BLOOM_SEED_2) % filter->nbits;
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		/* h1 + h2 + f(i) */
+		uint32		h = (h1 + i * h2) % filter->nbits;
+		uint32		byte = (h / 8);
+		uint32		bit = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (!(filter->data[byte] & (0x01 << bit)))
+			return false;
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash), but
+	 * let's keep the array just like inclusion and minman opclasses, for
+	 * consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+}			BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+									uint16 procnum);
+
+
+Datum
+brin_bloom_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 *
+	 * bloom indexes only store the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (BloomOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * brin_bloom_get_ndistinct
+ *		Determine the ndistinct value used to size bloom filter.
+ *
+ * Adjust the ndistinct value based on the pagesPerRange value. First,
+ * if it's negative, it's assumed to be relative to maximum number of
+ * tuples in the range (assuming each page gets MaxHeapTuplesPerPage
+ * tuples, which is likely a significant over-estimate). We also clamp
+ * the value, not to over-size the bloom filter unnecessarily.
+ *
+ * XXX We can only do this when the pagesPerRange value was supplied.
+ * If it wasn't, it has to be a read-only access to the index, in which
+ * case we don't really care. But perhaps we should fall-back to the
+ * default pagesPerRange value?
+ *
+ * XXX We might also fetch info about ndistinct estimate for the column,
+ * and compute the expected number of distinct values in a range. But
+ * that may be tricky due to data being sorted in various ways, so it
+ * seems better to rely on the upper estimate.
+ *
+ * XXX We might also calculate a better estimate of rows per BRIN range,
+ * instead of using MaxHeapTuplesPerPage (which probably produces values
+ * much higher than reality).
+ */
+static int
+brin_bloom_get_ndistinct(BrinDesc *bdesc, BloomOptions *opts)
+{
+	double		ndistinct;
+	double		maxtuples;
+	BlockNumber pagesPerRange;
+
+	pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	ndistinct = BloomGetNDistinctPerRange(opts);
+
+	Assert(BlockNumberIsValid(pagesPerRange));
+
+	maxtuples = MaxHeapTuplesPerPage * pagesPerRange;
+
+	/*
+	 * Similarly to n_distinct, negative values are relative - in this case to
+	 * maximum number of tuples in the page range (maxtuples).
+	 */
+	if (ndistinct < 0)
+		ndistinct = (-ndistinct) * maxtuples;
+
+	/*
+	 * Positive values are to be used directly, but we still apply a couple of
+	 * safeties to avoid using unreasonably small bloom filters.
+	 */
+	ndistinct = Max(ndistinct, BLOOM_MIN_NDISTINCT_PER_RANGE);
+
+	/*
+	 * And don't use more than the maximum possible number of tuples, in the
+	 * range, which would be entirely wasteful.
+	 */
+	ndistinct = Min(ndistinct, maxtuples);
+
+	return (int) ndistinct;
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the bloom filter specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_bloom_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from
+	 * BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(brin_bloom_get_ndistinct(bdesc, opts),
+							BloomGetFalsePositiveRate(opts));
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey		key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to
+				 * return the current page range if the minimum value in the
+				 * range <= scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters for now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			nbytes;
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure the filters use the same parameters */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+	Assert(filter_a->nhashes == filter_b->nhashes);
+	Assert((filter_a->nbits > 0) && (filter_a->nbits % 8 == 0));
+
+	nbytes = (filter_a->nbits) / 8;
+
+	/* simply OR the bitmaps */
+	for (i = 0; i < nbytes; i++)
+		filter_a->data[i] |= filter_b->data[i];
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+Datum
+brin_bloom_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(BloomOptions));
+
+	add_local_real_reloption(relopts, "n_distinct_per_range",
+							 "number of distinct items expected in a BRIN page range",
+							 BLOOM_DEFAULT_NDISTINCT_PER_RANGE,
+							 -1.0, INT_MAX, offsetof(BloomOptions, nDistinctPerRange));
+
+	add_local_real_reloption(relopts, "false_positive_rate",
+							 "desired false-positive rate for the bloom filters",
+							 BLOOM_DEFAULT_FALSE_POSITIVE_RATE,
+							 BLOOM_MIN_FALSE_POSITIVE_RATE,
+							 BLOOM_MAX_FALSE_POSITIVE_RATE,
+							 offsetof(BloomOptions, falsePositiveRate));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_bloom_summary_in
+ *		- input routine for type brin_bloom_summary.
+ *
+ * brin_bloom_summary is only used internally to represent summaries
+ * in BRIN bloom indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_bloom_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_bloom_summary stores the data in binary form and parsing text
+	 * input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_bloom_summary_out
+ *		- output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized into a bytea value, but we want
+ * to output something nicer humans can understand.
+ */
+Datum
+brin_bloom_summary_out(PG_FUNCTION_ARGS)
+{
+	BloomFilter *filter;
+	StringInfoData str;
+
+	/* detoast the data to get value with a full 4B header */
+	filter = (BloomFilter *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	appendStringInfo(&str, "mode: hashed  nhashes: %u  nbits: %u  nbits_set: %u",
+					 filter->nhashes, filter->nbits, filter->nbits_set);
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_bloom_summary_recv
+ *		- binary input routine for type brin_bloom_summary.
+ */
+Datum
+brin_bloom_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_brin_bloom_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_bloom_summary_send
+ *		- binary output routine for type brin_bloom_summary.
+ *
+ * BRIN bloom summaries are serialized in a bytea value (although the
+ * type is named differently), so let's just send that.
+ */
+Datum
+brin_bloom_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 3cf93fd381..2f18734235 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202103231
+#define CATALOG_VERSION_NO	202103232
 
 #endif
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0f7ff63669..04d678f96a 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,6 +1814,11 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
+# bloom bytea
+{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
+  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
+  amopmethod => 'brin' },
+
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1831,6 +1836,11 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
+# bloom "char"
+{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
+  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
+  amopmethod => 'brin' },
+
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1848,6 +1858,11 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
+# bloom name
+{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
+  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
+  amopmethod => 'brin' },
+
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -1994,6 +2009,20 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# bloom integer
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2011,6 +2040,11 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
+# bloom text
+{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
+  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
+  amopmethod => 'brin' },
+
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2028,6 +2062,11 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# bloom oid
+{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2045,6 +2084,11 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
+# tid oid
+{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2111,6 +2155,14 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# bloom float
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2128,6 +2180,11 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# bloom macaddr
+{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2145,6 +2202,11 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# bloom macaddr8
+{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2162,6 +2224,11 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# bloom inet
+{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2199,6 +2266,11 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
+# bloom character
+{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
+  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
+  amopmethod => 'brin' },
+
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2216,6 +2288,11 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# bloom time without time zone
+{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2362,6 +2439,20 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# bloom datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2379,6 +2470,11 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# bloom interval
+{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2396,6 +2492,11 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# bloom time with time zone
+{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2447,6 +2548,11 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# bloom numeric
+{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2464,6 +2570,11 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# bloom uuid
+{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -2525,6 +2636,11 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# bloom pg_lsn
+{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 36b5235c80..2af8af3f4e 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,6 +805,24 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom bytea
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
+
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -818,6 +836,24 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom "char"
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
+
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -831,6 +867,24 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom name
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
+
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -932,6 +986,58 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
+
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -945,6 +1051,24 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom text
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
+
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -957,6 +1081,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom oid
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
+
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -969,6 +1110,23 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom tid
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1019,6 +1177,45 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom float
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'hashfloat4' },
+
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'hashfloat8' },
+
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1033,6 +1230,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'hashmacaddr' },
+
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1047,6 +1264,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom macaddr8
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'hashmacaddr8' },
+
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1060,6 +1297,24 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom inet
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
+
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1094,6 +1349,26 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom character
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '11',
+  amproc => 'hashbpchar' },
+
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1107,6 +1382,24 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom time without time zone
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
+
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1214,6 +1507,62 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'timestamp_hash' },
+
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
+
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1228,6 +1577,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom interval
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'interval_hash' },
+
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1242,6 +1611,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom time with time zone
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'timetz_hash' },
+
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1282,6 +1671,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom numeric
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'hash_numeric' },
+
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1295,6 +1704,24 @@
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# bloom uuid
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
+
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1332,6 +1759,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# bloom pg_lsn
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_bloom_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_bloom_add_value' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_bloom_consistent' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_bloom_union' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'pg_lsn_hash' },
+
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 24b1433e1f..6a5bb58baf 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,130 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
+{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
+  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
+  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
+{ opcmethod => 'brin', opcname => 'char_bloom_ops',
+  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
+  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
+{ opcmethod => 'brin', opcname => 'name_bloom_ops',
+  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
+  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
+  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
+{ opcmethod => 'brin', opcname => 'text_bloom_ops',
+  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
+  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
+  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
+{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
+  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
+  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
+  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
+  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
+  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
+  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
+{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
+  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
+  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_bloom_ops',
+  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
+  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
+  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
+  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -335,18 +398,27 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
+  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
+  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
+  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 57e5aa0d8b..7cc3d59a8c 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,50 +182,88 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '4572',
+  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
+{ oid => '4573',
+  opfmethod => 'brin', opfname => 'text_bloom_ops' },
+{ oid => '4574',
+  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '4575',
+  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '4576',
+  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
+{ oid => '4577',
+  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
+{ oid => '4578',
+  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
+{ oid => '4579',
+  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '4580',
+  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
+{ oid => '4581',
+  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '4582',
+  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '4583',
+  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '4584',
+  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
+{ oid => '4585',
+  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
+{ oid => '4586',
+  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '4587',
+  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '4588',
+  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '4589',
+  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '4590',
+  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b9f4afba05..c89df733f7 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8238,6 +8238,26 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
+# BRIN bloom
+{ oid => '4591', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
+{ oid => '4592', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_add_value' },
+{ oid => '4593', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_bloom_consistent' },
+{ oid => '4594', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal',
+  prosrc => 'brin_bloom_union' },
+{ oid => '4595', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11411,4 +11431,18 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
+{ oid => '4596', descr => 'I/O',
+  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
+  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
+{ oid => '4597', descr => 'I/O',
+  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
+{ oid => '4598', descr => 'I/O',
+  proname => 'brin_bloom_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
+  prosrc => 'brin_bloom_summary_recv' },
+{ oid => '4599', descr => 'I/O',
+  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8959c2f53b..2a82a3e544 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,5 +679,10 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-
+{ oid => '4600',
+  descr => 'BRIN bloom summary',
+  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
+  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000000..32c56a996a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,428 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+ERROR:  value -1.1 out of bounds for option "n_distinct_per_range"
+DETAIL:  Valid values are between "-1.000000" and "2147483647.000000".
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+ERROR:  value 0.00009 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+ERROR:  value 0.26 out of bounds for option "false_positive_rate"
+DETAIL:  Valid values are between "0.000100" and "0.250000".
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 254ca06d3d..ef4b4444b9 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,6 +2037,7 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2100,7 +2101,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(123 rows)
+(124 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 7204fdb0b4..a7a5b22d4f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,8 +4993,9 @@ List of access methods
                    List of operator classes
   AM  | Input type | Storage type | Operator class | Default? 
 ------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_bloom_ops  | no
  brin | oid        |              | oid_minmax_ops | yes
-(1 row)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 0c74dc96a8..48ce3f7411 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,13 +67,14 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |     typname     
-------+-----------------
+ oid  |        typname        
+------+-----------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
+ 4600 | pg_brin_bloom_summary
  5017 | pg_mcv_list
-(4 rows)
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 70c38309d7..95927a7bae 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,6 +77,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Additional BRIN tests
+# ----------
+test: brin_bloom
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index d81d04136c..c02a981c78 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,6 +108,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000000..5d499208e3
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,376 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test bloom specific index options
+-- ndistinct must be >= -1.0
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(n_distinct_per_range = -1.1)
+);
+-- false_positive_rate must be between 0.0001 and 0.25
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.00009)
+);
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops(false_positive_rate = 0.26)
+);
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.30.2

0003-BRIN-minmax-multi-indexes-20210323.patchtext/x-patch; charset=UTF-8; name=0003-BRIN-minmax-multi-indexes-20210323.patchDownload
From 41912b7bd4db1eab6aaf63b0bec59a1cef8e271a Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Tue, 23 Mar 2021 01:04:06 +0100
Subject: [PATCH 3/5] BRIN minmax-multi indexes

Adds a BRIN minmax-multi opclass, summarizing each range into a list of
intervals, allowing more efficient handling of cases where the minmax
opclasses quickly degrade.

Instead of representing each range with a single interval, minmax-multi
opclasses store a list of intervals and points. This allows effective
handling of outliers and poorly correlated values.

The number of intervals/points per range may be configured using an
opclass parameter values_per_range. When building the summary, the
interval are merged based on distance, determined by the new support
BRIN procedure.

Author: Tomas Vondra <tomas.vondra@postgresql.org>
Reviewed-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Sokolov Yura <y.sokolov@postgrespro.ru>
Reviewed-by: John Naylor <john.naylor@enterprisedb.com>
Discussion: https://postgr.es/m/c1138ead-7668-f0e1-0638-c3be3237e812@2ndquadrant.com
Discussion: https://postgr.es/m/5d78b774-7e9c-c94e-12cf-fef51cc89b1a%402ndquadrant.com
---
 doc/src/sgml/brin.sgml                      |  280 +-
 src/backend/access/brin/Makefile            |    1 +
 src/backend/access/brin/brin_minmax_multi.c | 3036 +++++++++++++++++++
 src/backend/access/brin/brin_tuple.c        |   23 +-
 src/include/access/brin_tuple.h             |    8 +
 src/include/access/transam.h                |   10 +-
 src/include/catalog/catversion.h            |    2 +-
 src/include/catalog/pg_amop.dat             |  544 ++++
 src/include/catalog/pg_amproc.dat           |  600 +++-
 src/include/catalog/pg_opclass.dat          |   57 +
 src/include/catalog/pg_opfamily.dat         |   28 +
 src/include/catalog/pg_proc.dat             |   85 +
 src/include/catalog/pg_type.dat             |    6 +
 src/test/regress/expected/brin_multi.out    |  450 +++
 src/test/regress/expected/psql.out          |   13 +-
 src/test/regress/expected/type_sanity.out   |    7 +-
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/serial_schedule            |    1 +
 src/test/regress/sql/brin_multi.sql         |  403 +++
 19 files changed, 5526 insertions(+), 30 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index cb4f9b08b9..3ca8236272 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -116,7 +116,10 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
   in the indexed column within the range.  The <firstterm>inclusion</firstterm>
   operator classes store a value which includes the values in the indexed
   column within the range.  The <firstterm>bloom</firstterm> operator
-  classes build a Bloom filter for all values in the range.
+  classes build a Bloom filter for all values in the range.  The
+  <firstterm>minmax-multi</firstterm> operator classes store multiple
+  minimum and maximum values, representing values appearing in the indexed
+  column within the range.
  </para>
 
  <table id="brin-builtin-opclasses-table">
@@ -211,6 +214,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (date,date)</literal></entry></row>
     <row><entry><literal>&gt;= (date,date)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>date_minmax_multi_ops</literal></entry>
+     <entry><literal>= (date,date)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (date,date)</literal></entry></row>
+    <row><entry><literal>&lt;= (date,date)</literal></entry></row>
+    <row><entry><literal>&gt; (date,date)</literal></entry></row>
+    <row><entry><literal>&gt;= (date,date)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float4_bloom_ops</literal></entry>
      <entry><literal>= (float4,float4)</literal></entry>
@@ -225,6 +237,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
     <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float4,float4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt; (float4,float4)</literal></entry></row>
+    <row><entry><literal>&lt;= (float4,float4)</literal></entry></row>
+    <row><entry><literal>&gt;= (float4,float4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>float8_bloom_ops</literal></entry>
      <entry><literal>= (float8,float8)</literal></entry>
@@ -239,6 +260,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
     <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (float8,float8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&lt;= (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt; (float8,float8)</literal></entry></row>
+    <row><entry><literal>&gt;= (float8,float8)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="5"><literal>inet_inclusion_ops</literal></entry>
      <entry><literal>&lt;&lt; (inet,inet)</literal></entry>
@@ -263,6 +293,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
     <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><literal>= (inet,inet)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&lt;= (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt; (inet,inet)</literal></entry></row>
+    <row><entry><literal>&gt;= (inet,inet)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int2_bloom_ops</literal></entry>
      <entry><literal>= (int2,int2)</literal></entry>
@@ -277,6 +316,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
     <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int2_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int2,int2)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt; (int2,int2)</literal></entry></row>
+    <row><entry><literal>&lt;= (int2,int2)</literal></entry></row>
+    <row><entry><literal>&gt;= (int2,int2)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int4_bloom_ops</literal></entry>
      <entry><literal>= (int4,int4)</literal></entry>
@@ -291,6 +339,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
     <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int4_minmax_multi_ops</literal></entry>
+     <entry><literal>= (int4,int4)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt; (int4,int4)</literal></entry></row>
+    <row><entry><literal>&lt;= (int4,int4)</literal></entry></row>
+    <row><entry><literal>&gt;= (int4,int4)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>int8_bloom_ops</literal></entry>
      <entry><literal>= (bigint,bigint)</literal></entry>
@@ -305,6 +362,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
     <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>int8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (bigint,bigint)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt; (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&lt;= (bigint,bigint)</literal></entry></row>
+    <row><entry><literal>&gt;= (bigint,bigint)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>interval_bloom_ops</literal></entry>
      <entry><literal>= (interval,interval)</literal></entry>
@@ -319,6 +385,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
     <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><literal>= (interval,interval)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&lt;= (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt; (interval,interval)</literal></entry></row>
+    <row><entry><literal>&gt;= (interval,interval)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr_bloom_ops</literal></entry>
      <entry><literal>= (macaddr,macaddr)</literal></entry>
@@ -333,6 +408,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr,macaddr)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr,macaddr)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr,macaddr)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>macaddr8_bloom_ops</literal></entry>
      <entry><literal>= (macaddr8,macaddr8)</literal></entry>
@@ -347,6 +431,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
     <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><literal>= (macaddr8,macaddr8)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&lt;= (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt; (macaddr8,macaddr8)</literal></entry></row>
+    <row><entry><literal>&gt;= (macaddr8,macaddr8)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>name_bloom_ops</literal></entry>
      <entry><literal>= (name,name)</literal></entry>
@@ -375,6 +468,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
     <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>numeric_minmax_multi_ops</literal></entry>
+     <entry><literal>= (numeric,numeric)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&lt;= (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt; (numeric,numeric)</literal></entry></row>
+    <row><entry><literal>&gt;= (numeric,numeric)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>oid_bloom_ops</literal></entry>
      <entry><literal>= (oid,oid)</literal></entry>
@@ -389,6 +491,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
     <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>oid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (oid,oid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt; (oid,oid)</literal></entry></row>
+    <row><entry><literal>&lt;= (oid,oid)</literal></entry></row>
+    <row><entry><literal>&gt;= (oid,oid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>pg_lsn_bloom_ops</literal></entry>
      <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
@@ -403,6 +514,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
     <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><literal>= (pg_lsn,pg_lsn)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt; (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&lt;= (pg_lsn,pg_lsn)</literal></entry></row>
+    <row><entry><literal>&gt;= (pg_lsn,pg_lsn)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="13"><literal>range_inclusion_ops</literal></entry>
      <entry><literal>= (anyrange,anyrange)</literal></entry>
@@ -449,6 +569,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
     <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>tid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (tid,tid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt; (tid,tid)</literal></entry></row>
+    <row><entry><literal>&lt;= (tid,tid)</literal></entry></row>
+    <row><entry><literal>&gt;= (tid,tid)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamp_bloom_ops</literal></entry>
      <entry><literal>= (timestamp,timestamp)</literal></entry>
@@ -463,6 +592,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamp,timestamp)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamp,timestamp)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamp,timestamp)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timestamptz_bloom_ops</literal></entry>
      <entry><literal>= (timestamptz,timestamptz)</literal></entry>
@@ -477,6 +615,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
     <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timestamptz,timestamptz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt; (timestamptz,timestamptz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timestamptz,timestamptz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>time_bloom_ops</literal></entry>
      <entry><literal>= (time,time)</literal></entry>
@@ -491,6 +638,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (time,time)</literal></entry></row>
     <row><entry><literal>&gt;= (time,time)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>time_minmax_multi_ops</literal></entry>
+     <entry><literal>= (time,time)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (time,time)</literal></entry></row>
+    <row><entry><literal>&lt;= (time,time)</literal></entry></row>
+    <row><entry><literal>&gt; (time,time)</literal></entry></row>
+    <row><entry><literal>&gt;= (time,time)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>timetz_bloom_ops</literal></entry>
      <entry><literal>= (timetz,timetz)</literal></entry>
@@ -505,6 +661,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
     <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><literal>= (timetz,timetz)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&lt;= (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt; (timetz,timetz)</literal></entry></row>
+    <row><entry><literal>&gt;= (timetz,timetz)</literal></entry></row>
+
     <row>
      <entry valign="middle"><literal>uuid_bloom_ops</literal></entry>
      <entry><literal>= (uuid,uuid)</literal></entry>
@@ -519,6 +684,15 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
     <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
     <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
 
+    <row>
+     <entry valign="middle" morerows="4"><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><literal>= (uuid,uuid)</literal></entry>
+    </row>
+    <row><entry><literal>&lt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt; (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&lt;= (uuid,uuid)</literal></entry></row>
+    <row><entry><literal>&gt;= (uuid,uuid)</literal></entry></row>
+
     <row>
      <entry valign="middle" morerows="4"><literal>varbit_minmax_ops</literal></entry>
      <entry><literal>= (varbit,varbit)</literal></entry>
@@ -537,8 +711,8 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    <para>
     Some of the built-in operator classes allow specifying parameters affecting
     behavior of the operator class.  Each operator class has its own set of
-    allowed parameters.  Only the <literal>bloom</literal> operator class
-    allows specifying parameters:
+    allowed parameters.  Only the <literal>bloom</literal> and <literal>minmax-multi</literal>
+    operator classes allow specifying parameters:
    </para>
 
    <para>
@@ -577,6 +751,25 @@ LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was
    </varlistentry>
 
    </variablelist>
+
+   <para>
+    <acronym>minmax-multi</acronym> operator classes accept these parameters:
+   </para>
+
+   <variablelist>
+   <varlistentry>
+    <term><literal>values_per_range</literal></term>
+    <listitem>
+    <para>
+     Defines the maximum number of values stored by <acronym>BRIN</acronym>
+     minmax indexes to summarize a block range. Each value may represent
+     either a point, or a boundary of an interval. Values must be between
+     8 and 256, and the default value is 32.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   </variablelist>
   </sect2>
 
 </sect1>
@@ -702,13 +895,14 @@ typedef struct BrinOpcInfo
     </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support functions' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, minmax-multi, inclusion and bloom.  Operator class definitions
+  using them are shipped for in-core data types as appropriate.  Additional
+  operator classes can be defined by the user for other data types using
+  equivalent definitions, without having to write any source code;
+  appropriate catalog entries being declared is enough.  Note that
+  assumptions about the semantics of operator strategies are embedded in the
+  support functions' source code.
  </para>
 
  <para>
@@ -1005,6 +1199,72 @@ typedef struct BrinOpcInfo
     and return a hash of the value.
  </para>
 
+ <para>
+  The minmax-multi operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, minmax-multi allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the minmax-multi support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-minmax-multi-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-minmax-multi-table">
+  <title>Procedure and Support Numbers for minmax-multi Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
  <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 6b56131215..a386cb71f1 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
+	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000000..1ca86b7fb7
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,3036 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually make the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With minmax-multi opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 32 (see values_per_range reloption). Collapsed intervals
+ * (with equal minimum and maximum value) are stored as a single value,
+ * while regular intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 50% of maximum values), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ * The compactions (reducing the number of values) is fairly expensive, as
+ * it requires calling the distance functions, sorting etc. So when building
+ * the summary, we use a significantly larger buffer, and only enforce the
+ * exact limit at the very end. This improves performance, and it also helps
+ * with building better ranges (due to the greedy approach).
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+/* needef for PGSQL_AF_INET */
+#include <sys/socket.h>
+
+#include "access/genam.h"
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_amop.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/uuid.h"
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values */
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+/*
+ * Sizing the insert buffer - we use 10x the number of values specified
+ * in the reloption, but we cap it to 8192 not to get too large. When
+ * the buffer gets full, we reduce the number of values by half.
+ */
+#define		MINMAX_BUFFER_FACTOR			10
+#define		MINMAX_BUFFER_MIN				256
+#define		MINMAX_BUFFER_MAX				8192
+#define		MINMAX_BUFFER_LOAD_FACTOR		0.5
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+}			MinmaxMultiOpaque;
+
+/*
+ * Storage type for BRIN's minmax reloptions
+ */
+typedef struct MinMaxMultiOptions
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int			valuesPerRange; /* number of values per range */
+} MinMaxMultiOptions;
+
+#define MINMAX_MULTI_DEFAULT_VALUES_PER_PAGE           32
+
+#define MinMaxMultiGetValuesPerRange(opts) \
+		((opts) && (((MinMaxMultiOptions *) (opts))->valuesPerRange != 0) ? \
+		 ((MinMaxMultiOptions *) (opts))->valuesPerRange : \
+		 MINMAX_MULTI_DEFAULT_VALUES_PER_PAGE)
+
+#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0))
+
+/*
+ * The summary of minmax-multi indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than maxvalues values (as set by values_per_range
+ * reloption). If needed we merge some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for maxvalues elements. This should be fine as long as the
+ * maxvalues is reasonably small (64 seems fine), which is the case
+ * thanks to values_per_range reloption being limited to 256.
+ */
+typedef struct Ranges
+{
+	/* Cache information that we need quite often. */
+	Oid			typid;
+	Oid			colloid;
+	AttrNumber	attno;
+	FmgrInfo   *cmp;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nsorted;		/* number of sorted values (ranges + points) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/*
+	 * We simply add the values into a large buffer, without any expensive
+	 * steps (sorting, deduplication, ...). The buffer is a multiple of the
+	 * target number of values, so the compaction happen less often,
+	 * amortizing the costs. We keep the actual target and compact to the
+	 * requested number of values at the very end, before serializing to
+	 * on-disk representation.
+	 */
+	/* requested number of values */
+	int			target_maxvalues;
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum		values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, with a simple header
+ * with basic metadata, followed by the boundary values. It has a varlena
+ * header, so can be treated as varlena directly.
+ *
+ * See range_serialize/range_deserialize for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32		vl_len_;
+
+	/* type of values stored in the data array */
+	Oid			typid;
+
+	/* (2*nranges + nvalues) <= maxvalues */
+	int			nranges;		/* number of ranges in the array (stored) */
+	int			nvalues;		/* number of values in the data array (all) */
+	int			maxvalues;		/* maximum number of values (reloption) */
+
+	/* contains the actual data */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range);
+
+static Ranges *range_deserialize(int maxvalues, SerializedRanges *range);
+
+
+/*
+ * Used to represent ranges expanded to make merging and combining easier.
+ *
+ * Each expanded range is essentially an interval, represented by min/max
+ * values, along with a flag whether it's a collapsed range (in which case
+ * the min and max values are equal). We have the flag to handle by-ref
+ * data types - we can't simply compare the datums, and this saves some
+ * calls to the type-specific comparator function.
+ */
+typedef struct ExpandedRange
+{
+	Datum		minval;			/* lower boundary */
+	Datum		maxval;			/* upper boundary */
+	bool		collapsed;		/* true if minval==maxval */
+} ExpandedRange;
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of extended ranges).
+ */
+typedef struct DistanceValue
+{
+	int			index;
+	double		value;
+} DistanceValue;
+
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+										   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+													uint16 attno, Oid subtype,
+													uint16 strategynum);
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+}			compare_context;
+
+static int	compare_values(const void *a, const void *b, void *arg);
+
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+	int			i;
+	Datum		lt;
+
+	for (i = 0; i < (nvalues - 1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i + 1]);
+		Assert(DatumGetBool(lt));
+	}
+}
+#endif
+
+/*
+ * Comprehensive check of the Ranges structure.
+ */
+static void
+AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+
+	/* some basic sanity checks */
+	Assert(ranges->nranges >= 0);
+	Assert(ranges->nsorted >= 0);
+	Assert(ranges->nvalues >= ranges->nsorted);
+	Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues);
+	Assert(ranges->typid != InvalidOid);
+
+	/*
+	 * First the ranges - there are 2*nranges boundary values, and the values
+	 * have to be strictly ordered (equal values would mean the range is
+	 * collapsed, and should be stored as a point). This also guarantees that
+	 * the ranges do not overlap.
+	 */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2 * ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2 * ranges->nranges],
+					 ranges->nsorted);
+
+	/*
+	 * Check that none of the values are not covered by ranges (both sorted
+	 * and unsorted)
+	 */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		compar;
+		int			start,
+					end;
+		Datum		minvalue,
+					maxvalue;
+
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nranges == 0)
+			break;
+
+		minvalue = ranges->values[0];
+		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+		/* smaller than the smallest value in the first range */
+		if (DatumGetBool(compar))
+			continue;
+
+		/*
+		 * Is the value greater than the maxval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+		/* larger than the largest value in the last range */
+		if (DatumGetBool(compar))
+			continue;
+
+		start = 0;				/* first range */
+		end = ranges->nranges - 1;	/* last range */
+		while (true)
+		{
+			int			midpoint = (start + end) / 2;
+
+			/* this means we ran out of ranges in the last step */
+			if (start > end)
+				break;
+
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2 * midpoint];
+			maxvalue = ranges->values[2 * midpoint + 1];
+
+			/*
+			 * Is the value smaller than the minval? If yes, we'll recurse to
+			 * the left side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+			{
+				end = (midpoint - 1);
+				continue;
+			}
+
+			/*
+			 * Is the value greater than the minval? If yes, we'll recurse to
+			 * the right side of range array.
+			 */
+			compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+			{
+				start = (midpoint + 1);
+				continue;
+			}
+
+			/* hey, we found a matching range */
+			Assert(false);
+		}
+	}
+
+	/* and values in the unsorted part must not be in sorted part */
+	for (i = ranges->nsorted; i < ranges->nvalues; i++)
+	{
+		compare_context cxt;
+		Datum		value = ranges->values[2 * ranges->nranges + i];
+
+		if (ranges->nsorted == 0)
+			break;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		Assert(bsearch_arg(&value, &ranges->values[2 * ranges->nranges],
+						   ranges->nsorted, sizeof(Datum),
+						   compare_values, (void *) &cxt) == NULL);
+	}
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertCheckExpandedRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						  Form_pg_attribute attr, ExpandedRange *ranges,
+						  int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int			i;
+	FmgrInfo   *eq;
+	FmgrInfo   *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum		r;
+		Datum		minval = ranges[i].minval;
+		Datum		maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else					/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e. upper <
+	 * lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges - 1; i++)
+	{
+		Datum		r;
+		Datum		maxval = ranges[i].maxval;
+		Datum		minval = ranges[i + 1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * enough space for the maximum number of values (so as not to have to do
+ * repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(int maxvalues)
+{
+	Size		len;
+	Ranges	   *ranges;
+
+	Assert(maxvalues > 0);
+
+	len = offsetof(Ranges, values); /* fixed header */
+	len += maxvalues * sizeof(Datum);	/* Datum values */
+
+	ranges = (Ranges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	return ranges;
+}
+
+
+/*
+ * range_deduplicate_values
+ *		Deduplicate the part with values in the simple points.
+ *
+ * This is meant to be a cheaper way of reducing the size of the ranges. It
+ * does not touch the ranges, and only sorts the other values - it does not
+ * call the distance functions, which may be quite expensive, etc.
+ *
+ * We do know the values are not duplicate with the ranges, because we check
+ * that before adding a new value. Same for the sorted part of values.
+ */
+static void
+range_deduplicate_values(Ranges *range)
+{
+	int			i,
+				n;
+	int			start;
+	compare_context cxt;
+
+	/*
+	 * If there are no unsorted values, we're done (this probably can't
+	 * happen, as we're adding values to unsorted part).
+	 */
+	if (range->nsorted == range->nvalues)
+		return;
+
+	/* sort the values */
+	cxt.colloid = range->colloid;
+	cxt.cmpFn = range->cmp;
+
+	/* the values start right after the ranges (which are always sorted) */
+	start = 2 * range->nranges;
+
+	/*
+	 * XXX This might do a merge sort, to leverage that the first part of the
+	 * array is already sorted. If the sorted part is large, it might be quite
+	 * a bit faster.
+	 */
+	qsort_arg(&range->values[start],
+			  range->nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	n = 1;
+	for (i = 1; i < range->nvalues; i++)
+	{
+		/* same as preceding value, so store it */
+		if (compare_values(&range->values[start + i - 1],
+						   &range->values[start + i],
+						   (void *) &cxt) == 0)
+			continue;
+
+		range->values[start + n] = range->values[start + i];
+
+		n++;
+	}
+
+	/* now all the values are sorted */
+	range->nvalues = n;
+	range->nsorted = n;
+
+	AssertCheckRanges(range, range->cmp, range->colloid);
+}
+
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range)
+{
+	Size		len;
+	int			nvalues;
+	SerializedRanges *serialized;
+	Oid			typid;
+	int			typlen;
+	bool		typbyval;
+
+	int			i;
+	char	   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nsorted >= 0);
+	Assert(range->nvalues >= 0);
+	Assert(range->maxvalues > 0);
+	Assert(range->target_maxvalues > 0);
+
+	/* at this point the range should be compacted to the target size */
+	Assert(2 * range->nranges + range->nvalues <= range->target_maxvalues);
+
+	Assert(range->target_maxvalues <= range->maxvalues);
+
+	/* range boundaries are always sorted */
+	Assert(range->nvalues >= range->nsorted);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2 * range->nranges + range->nvalues;
+
+	typid = range->typid;
+	typbyval = get_typbyval(typid);
+	typlen = get_typlen(typid);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges, data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (typlen == -1)			/* varlena */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (typlen == -2)		/* cstring */
+	{
+		int			i;
+
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else						/* fixed-length types (even by-reference) */
+	{
+		Assert(typlen > 0);
+		len += nvalues * typlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->typid = typid;
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+	serialized->maxvalues = range->target_maxvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation this
+	 * depends on the particular data type).
+	 */
+	ptr = serialized->data;		/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), typlen);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			int			tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			int			tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + len));
+	}
+
+	/* exact size */
+	Assert(ptr == ((char *) serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(int maxvalues, SerializedRanges *serialized)
+{
+	int			i,
+				nvalues;
+	char	   *ptr;
+	bool		typbyval;
+	int			typlen;
+
+	Ranges	   *range;
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+	Assert(serialized->maxvalues > 0);
+
+	nvalues = 2 * serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= serialized->maxvalues);
+	Assert(serialized->maxvalues <= maxvalues);
+
+	range = minmax_multi_init(maxvalues);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+	range->nsorted = serialized->nvalues;
+	range->maxvalues = maxvalues;
+	range->target_maxvalues = serialized->maxvalues;
+
+	range->typid = serialized->typid;
+
+	typbyval = get_typbyval(serialized->typid);
+	typlen = get_typlen(serialized->typid);
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need to copy
+	 * the values and will instead just point the values to the serialized
+	 * varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (typbyval)			/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, typlen);
+			ptr += typlen;
+		}
+		else if (typlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += typlen;
+		}
+		else if (typlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (typlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *) serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *) serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+/*
+ * compare_expanded_ranges
+ *	  Compare the expanded ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_expanded_ranges(const void *a, const void *b, void *arg)
+{
+	ExpandedRange *ra = (ExpandedRange *) a;
+	ExpandedRange *rb = (ExpandedRange *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	/* first compare minvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->minval, rb->minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->minval, ra->minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * compare_values
+ *	  Compare the values.
+ */
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum	   *da = (Datum *) a;
+	Datum	   *db = (Datum *) b;
+	Datum		r;
+
+	compare_context *cxt = (compare_context *) arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *da, *db);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, *db, *da);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Check if the new value matches one of the existing ranges.
+ */
+static bool
+has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges,
+				   Datum newval, AttrNumber attno, Oid typid)
+{
+	Datum		compar;
+
+	Datum		minvalue = ranges->values[0];
+	Datum		maxvalue = ranges->values[2 * ranges->nranges - 1];
+
+	FmgrInfo   *cmpLessFn;
+	FmgrInfo   *cmpGreaterFn;
+
+	/* binary search on ranges */
+	int			start,
+				end;
+
+	if (ranges->nranges == 0)
+		return false;
+
+	/*
+	 * Otherwise, need to compare the new value with boundaries of all the
+	 * ranges. First check if it's less than the absolute minimum, which is
+	 * the first value in the array.
+	 */
+	cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												   BTLessStrategyNumber);
+	compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+	/* smaller than the smallest value in the range list */
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * And now compare it to the existing maximum (last value in the data
+	 * array). But only if we haven't already ruled out a possible match in
+	 * the minvalue check.
+	 */
+	cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													  BTGreaterStrategyNumber);
+	compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+	if (DatumGetBool(compar))
+		return false;
+
+	/*
+	 * So we know it's in the general min/max, the question is whether it
+	 * falls in one of the ranges or gaps. We'll do a binary search on
+	 * individual ranges - for each range we check equality (value falls into
+	 * the range), and then check ranges either above or below the current
+	 * range.
+	 */
+	start = 0;					/* first range */
+	end = (ranges->nranges - 1);	/* last range */
+	while (true)
+	{
+		int			midpoint = (start + end) / 2;
+
+		/* this means we ran out of ranges in the last step */
+		if (start > end)
+			return false;
+
+		/* copy the min/max values from the ranges */
+		minvalue = ranges->values[2 * midpoint];
+		maxvalue = ranges->values[2 * midpoint + 1];
+
+		/*
+		 * Is the value smaller than the minval? If yes, we'll recurse to the
+		 * left side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in this range */
+		if (DatumGetBool(compar))
+		{
+			end = (midpoint - 1);
+			continue;
+		}
+
+		/*
+		 * Is the value greater than the minval? If yes, we'll recurse to the
+		 * right side of range array.
+		 */
+		compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue);
+
+		/* larger than the largest value in this range */
+		if (DatumGetBool(compar))
+		{
+			start = (midpoint + 1);
+			continue;
+		}
+
+		/* hey, we found a matching range */
+		return true;
+	}
+
+	return false;
+}
+
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * The last parameter (full) determines whether we need to search all the
+ * values, including the unsorted part. With full=false, the unsorted part
+ * is not searched, which may produce false negatives and duplicate values
+ * (in the unsorted part only), but when we're building the range that's
+ * fine - we'll deduplicate before serialization, and it can only happen
+ * if there already are unsorted values (so it was already modified).
+ *
+ * Serialized ranges don't have any unsorted values, so this can't cause
+ * false negatives during querying.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+					 AttrNumber attno, Form_pg_attribute attr,
+					 Ranges *ranges, Datum newval, bool full)
+{
+	int			i;
+	FmgrInfo   *cmpEqualFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid))
+		return true;
+
+	cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+													BTEqualStrategyNumber);
+
+	/*
+	 * There is no matching range, so let's inspect the sorted values.
+	 *
+	 * We do a sequential search for small number of values, and binary search
+	 * once we have more than 16 values. This threshold is somewhat arbitrary,
+	 * as it depends on how expensive the comparison function is.
+	 *
+	 * XXX If we use the threshold here, maybe we should do the same thing in
+	 * has_matching_range? Or maybe we should do the bin search all the time?
+	 *
+	 * XXX We could use the same optimization as for ranges, to check if the
+	 * value is between min/max, to maybe rule out all sorted values without
+	 * having to inspect all of them.
+	 */
+	if (ranges->nsorted >= 16)
+	{
+		compare_context cxt;
+
+		cxt.colloid = ranges->colloid;
+		cxt.cmpFn = ranges->cmp;
+
+		if (bsearch_arg(&newval, &ranges->values[2 * ranges->nranges],
+						ranges->nsorted, sizeof(Datum),
+						compare_values, (void *) &cxt) != NULL)
+			return true;
+	}
+	else
+	{
+		for (i = 2 * ranges->nranges; i < 2 * ranges->nranges + ranges->nsorted; i++)
+		{
+			Datum		compar;
+
+			compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+			/* found an exact match */
+			if (DatumGetBool(compar))
+				return true;
+		}
+	}
+
+	/* If not asked to inspect the unsorted part, we're done. */
+	if (!full)
+		return false;
+
+	/* Inspect the unsorted part. */
+	for (i = 2 * ranges->nranges + ranges->nsorted; i < 2 * ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum		compar;
+
+		compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+/*
+ * Expand ranges from Ranges into ExpandedRange array. This expects the
+ * eranges to be pre-allocated and with the correct size - there needs to be
+ * (nranges + nvalues) elements.
+ *
+ * The order of expanded ranges is arbitrary. We do expand the ranges first,
+ * and this part is sorted. But then we expand the values, and this part may
+ * be unsorted.
+ */
+static void
+fill_expanded_ranges(ExpandedRange *eranges, int neranges, Ranges *ranges)
+{
+	int			idx;
+	int			i;
+
+	/* Check that the output array has the right size. */
+	Assert(neranges == (ranges->nranges + ranges->nvalues));
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * i];
+		eranges[idx].maxval = ranges->values[2 * i + 1];
+		eranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		eranges[idx].minval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].maxval = ranges->values[2 * ranges->nranges + i];
+		eranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= neranges);
+	}
+
+	/* Did we produce the expected number of elements? */
+	Assert(idx == neranges);
+
+	return;
+}
+
+/*
+ * Sort and deduplicate expanded ranges.
+ *
+ * The ranges may be deduplicated - we're simply appending values, without
+ * checking for duplicates etc. So maybe the deduplication will reduce the
+ * number of ranges enough, and we won't have to compute the distances etc.
+ *
+ * Returns the number of expanded ranges.
+ */
+static int
+sort_expanded_ranges(FmgrInfo *cmp, Oid colloid,
+					 ExpandedRange *eranges, int neranges)
+{
+	int			n;
+	int			i;
+	compare_context cxt;
+
+	Assert(neranges > 0);
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/*
+	 * XXX We do qsort on all the values, but we could also leverage the fact
+	 * that some of the input data is already sorted (all the ranges and maybe
+	 * some of the points) and do merge sort.
+	 */
+	qsort_arg(eranges, neranges, sizeof(ExpandedRange),
+			  compare_expanded_ranges, (void *) &cxt);
+
+	/*
+	 * Deduplicate the ranges - simply compare each range to the preceding
+	 * one, and skip the duplicate ones.
+	 */
+	n = 1;
+	for (i = 1; i < neranges; i++)
+	{
+		/* if the current range is equal to the preceding one, do nothing */
+		if (!compare_expanded_ranges(&eranges[i - 1], &eranges[i], (void *) &cxt))
+			continue;
+
+		/* otherwise copy it to n-th place (if not already there) */
+		if (i != n)
+			memcpy(&eranges[n], &eranges[i], sizeof(ExpandedRange));
+
+		n++;
+	}
+
+	Assert((n > 0) && (n <= neranges));
+
+	return n;
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the expanded ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_overlapping_ranges(FmgrInfo *cmp, Oid colloid,
+						 ExpandedRange *eranges, int neranges)
+{
+	int			idx;
+
+	/* Merge ranges (idx) and (idx+1) if they overlap. */
+	idx = 0;
+	while (idx < (neranges - 1))
+	{
+		Datum		r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap if (minval
+		 * < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges are
+		 * ordered, so there are no more overlaps, because all the remaining
+		 * ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they overlap only partially.
+		 * So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  eranges[idx].maxval,
+							  eranges[idx + 1].maxval);
+
+		if (DatumGetBool(r))
+			eranges[idx].maxval = eranges[idx + 1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectively of the
+		 * previous state).
+		 */
+		eranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the remaining
+		 * ranges by 1. There are neranges elements, and we need to move
+		 * elements from (idx+2). That means the number of elements to move is
+		 * [ncranges - (idx+2)].
+		 */
+		memmove(&eranges[idx + 1], &eranges[idx + 2],
+				(neranges - (idx + 2)) * sizeof(ExpandedRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range, as
+		 * it might overlap with additional ranges thanks to the merge).
+		 */
+		neranges--;
+	}
+
+	return neranges;
+}
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ * This is intentionally sorting the distances in descending order, i.e.
+ * the longer gaps will be at the front.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *) a;
+	DistanceValue *db = (DistanceValue *) b;
+
+	if (da->value < db->value)
+		return 1;
+	else if (da->value > db->value)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Given an array of expanded ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (max-min) for pairs
+ * of consecutive ranges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_expanded_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			ndistances;
+	DistanceValue *distances;
+
+	Assert(neranges >= 2);
+
+	ndistances = (neranges - 1);
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * ndistances);
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges so
+	 * that we can sort them once.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		Datum		a1,
+					a2,
+					r;
+
+		a1 = eranges[i].maxval;
+		a2 = eranges[i + 1].minval;
+
+		/* compute length of the gap (between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index of the gap the distance is for */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/*
+	 * Sort the distances in descending order, so that the longest gaps are at
+	 * the front.
+	 */
+	pg_qsort(distances, ndistances, sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds expanded ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).  This expanded
+ * representation makes the processing a bit easier, as it allows handling
+ * ranges and points the same way.
+ *
+ * We sort and deduplicate the expanded ranges - this is necessary, because
+ * the points may be unsorted. And moreover the two parts (ranges and
+ * points) are sorted on their own.
+ */
+static ExpandedRange *
+build_expanded_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					  int *nranges)
+{
+	int			neranges;
+	ExpandedRange *eranges;
+
+	/* both ranges and points are expanded into a separate element */
+	neranges = ranges->nranges + ranges->nvalues;
+
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges */
+	fill_expanded_ranges(eranges, neranges, ranges);
+
+	/* sort and deduplicate the expanded ranges */
+	neranges = sort_expanded_ranges(cmp, colloid, eranges, neranges);
+
+	/* remember how many cranges we built */
+	*nranges = neranges;
+
+	return eranges;
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Counts boundary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(ExpandedRange *cranges, int ncranges)
+{
+	int			i;
+	int			count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+#endif
+
+/*
+ * reduce_expanded_ranges
+ *		reduce the ranges until the number of values is low enough
+ *
+ * Combines ranges until the number of boundary values drops below the
+ * threshold specified by max_values. This happens by merging enough
+ * ranges by distance between them.
+ *
+ * Returns the number of result ranges.
+ *
+ * We simply use the global min/max and then add boundaries for enough
+ * largest gaps. Each gap adds 2 values, so we simply use (target/2-1)
+ * distances. Then we simply sort all the values - each two values are
+ * a boundary of a range (possibly collapsed).
+ *
+ * XXX Some of the ranges may be collapsed (i.e. the min/max values are
+ * equal), but we ignore that for now. We could repeat the process,
+ * adding a couple more gaps recursively.
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ *
+ * XXX Or maybe we could just handle this by using random value as a
+ * tie-break, or by adding random noise to the actual distance.
+ */
+static int
+reduce_expanded_ranges(ExpandedRange *eranges, int neranges,
+					   DistanceValue *distances, int max_values,
+					   FmgrInfo *cmp, Oid colloid)
+{
+	int			i;
+	int			nvalues;
+	Datum	   *values;
+
+	compare_context cxt;
+
+	/* total number of gaps between ranges */
+	int			ndistances = (neranges - 1);
+
+	/* number of gaps to keep */
+	int			keep = (max_values / 2 - 1);
+
+	/*
+	 * Maybe we have sufficiently low number of ranges already?
+	 *
+	 * XXX This should happen before we actually do the expensive stuff like
+	 * sorting, so maybe this should be just an assert.
+	 */
+	if (keep >= ndistances)
+		return neranges;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	/* allocate space for the boundary values */
+	nvalues = 0;
+	values = (Datum *) palloc(sizeof(Datum) * max_values);
+
+	/* add the global min/max values, from the first/last range */
+	values[nvalues++] = eranges[0].minval;
+	values[nvalues++] = eranges[neranges - 1].maxval;
+
+	/* add boundary values for enough gaps */
+	for (i = 0; i < keep; i++)
+	{
+		/* index of the gap between (index) and (index+1) ranges */
+		int			index = distances[i].index;
+
+		Assert((index >= 0) && ((index + 1) < neranges));
+
+		/* add max from the preceding range, minval from the next one */
+		values[nvalues++] = eranges[index].maxval;
+		values[nvalues++] = eranges[index + 1].minval;
+
+		Assert(nvalues <= max_values);
+	}
+
+	/* We should have even number of range values. */
+	Assert(nvalues % 2 == 0);
+
+	/*
+	 * Sort the values using the comparator function, and form ranges from the
+	 * sorted result.
+	 */
+	qsort_arg(values, nvalues, sizeof(Datum),
+			  compare_values, (void *) &cxt);
+
+	/* We have nvalues boundary values, which means nvalues/2 ranges. */
+	for (i = 0; i < (nvalues / 2); i++)
+	{
+		eranges[i].minval = values[2 * i];
+		eranges[i].maxval = values[2 * i + 1];
+
+		/* if the boundary values are the same, it's a collapsed range */
+		eranges[i].collapsed = (compare_values(&values[2 * i],
+											   &values[2 * i + 1],
+											   &cxt) == 0);
+	}
+
+	return (nvalues / 2);
+}
+
+/*
+ * Store the boundary values from ExpandedRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_expanded_ranges(Ranges *ranges, ExpandedRange *eranges, int neranges)
+{
+	int			i;
+	int			idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (!eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->values[idx++] = eranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < neranges; i++)
+	{
+		if (eranges[i].collapsed)
+		{
+			ranges->values[idx++] = eranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+
+	/* all the values are sorted */
+	ranges->nsorted = ranges->nvalues;
+
+	Assert(count_values(eranges, neranges) == 2 * ranges->nranges + ranges->nvalues);
+	Assert(2 * ranges->nranges + ranges->nvalues <= ranges->maxvalues);
+}
+
+
+/*
+ * Consider freeing space in the ranges. Checks if there's space for at least
+ * one new value, and performs compaction if needed.
+ *
+ * Returns true if the value was actually modified.
+ */
+static bool
+ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *range)
+{
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	/*
+	 * If there is free space in the buffer, we're done without having to
+	 * modify anything.
+	 */
+	if (2 * range->nranges + range->nvalues < range->maxvalues)
+		return false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* deduplicate values, if there's unsorted part */
+	range_deduplicate_values(range);
+
+	/*
+	 * did we reduce enough free space by just the deduplication?
+	 *
+	 * We don't simply check against range->maxvalues again. The deduplication
+	 * might have freed very little space (e.g. just one value), forcing us to
+	 * do depuplication very often. In that case it's better to do compaction
+	 * and reduce more space.
+	 */
+	if (2 * range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR)
+		return true;
+
+	/*
+	 * We need to combine some of the existing ranges, to reduce the number of
+	 * values we have to store.
+	 *
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it (we might have
+	 * to do this repeatedly, even for a single BRIN page range). Otherwise
+	 * we'd have problems e.g. when building new indexes. So we use a memory
+	 * context and make sure we free the memory at the end (so if we call the
+	 * distance function many times, it might be an issue, but meh).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, colloid, range, &neranges);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * Combine ranges until we release at least 50% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not use
+	 * too low or high value.
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR,
+									  cmpFn, colloid);
+
+	/* Make sure we've sufficiently reduced the number of ranges. */
+	Assert(count_values(eranges, neranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR);
+
+	/* decompose the expanded ranges into regular ranges and single values */
+	store_expanded_ranges(range, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Did we break the ranges somehow? */
+	AssertCheckRanges(range, cmpFn, colloid);
+
+	return true;
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the minmax-multi range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn;
+	bool		modified = false;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/*
+	 * Make sure there's enough free space in the buffer. We only trigger this
+	 * when the buffer is full, which means it had to be modified as we size
+	 * it to be larger than what is stored on disk.
+	 *
+	 * This needs to happen before we check if the value is contained in the
+	 * range, because the value might be in the unsorted part, and we don't
+	 * check that in range_contains_value. The deduplication would then move
+	 * it to the sorted part, and we'd add the value too, which violates the
+	 * rule that we never have duplicates with the ranges or sorted values.
+	 *
+	 * We might also deduplicate and recheck if the value is contained, but
+	 * that seems like an overkill. We'd need to deduplicate anyway, so why
+	 * not do it now.
+	 */
+	modified = ensure_free_space_in_buffer(bdesc, colloid,
+										   attno, attr, ranges);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit values_per_range, and then do the
+	 * deduplication in a batch, hoping for better efficiency. But that would
+	 * mean we actually modify the range every time, which means having to
+	 * serialize the value, which does palloc, walks the values, copies them,
+	 * etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming the
+	 * comparator function is not very expensive.
+	 *
+	 * This also implies the values array can't contain duplicate values.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval, false))
+		return modified;
+
+	/* Make a copy of the value, if needed. */
+	newval = datumCopy(newval, attr->attbyval, attr->attlen);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we do a
+	 * simple insertion sort. We could do something more elaborate, e.g. by
+	 * sorting the values only now and then, but for small counts (e.g. when
+	 * maxvalues is 64) this should be fine.
+	 */
+	ranges->values[2 * ranges->nranges + ranges->nvalues] = newval;
+	ranges->nvalues++;
+
+	/* If we added the first value, we can consider it as sorted. */
+	if (ranges->nvalues == 1)
+		ranges->nsorted = 1;
+
+	/*
+	 * Check we haven't broken the ordering of boundary values (checks both
+	 * parts, but that doesn't hurt).
+	 */
+	AssertCheckRanges(ranges, cmpFn, colloid);
+
+	/* Check the range contains the value we just added. */
+	Assert(range_contains_value(bdesc, colloid, attno, attr, ranges, newval, true));
+
+	/* yep, we've modified the range */
+	return true;
+}
+
+/*
+ * Generate range representation of data collected during "batch mode".
+ * This is similar to reduce_expanded_ranges, except that we can't assume
+ * the values are sorted and there may be duplicate values.
+ */
+static void
+compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* expanded ranges */
+	ExpandedRange *eranges;
+	int			neranges;
+	DistanceValue *distances;
+
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	/*
+	 * Do we need to actually compactify anything?
+	 *
+	 * There are two reasons why compaction may be needed - firstly, there may
+	 * be too many values, or some of the values may be unsorted.
+	 */
+	if ((ranges->nranges * 2 + ranges->nvalues <= max_values) &&
+		(ranges->nsorted == ranges->nvalues))
+		return;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid,
+											   BTLessStrategyNumber);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, ranges->attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* build the expanded ranges */
+	eranges = build_expanded_ranges(cmpFn, ranges->colloid, ranges, &neranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, ranges->colloid,
+								eranges, neranges);
+
+	/*
+	 * Combine ranges until we get below max_values. We don't use any scale
+	 * factor, because this is used during serialization, and we don't expect
+	 * more tuples to be inserted anytime soon.
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  max_values, cmpFn, ranges->colloid);
+
+	Assert(count_values(eranges, neranges) <= max_values);
+
+	/* transform back into regular ranges and single values */
+	store_expanded_ranges(ranges, eranges, neranges);
+
+	/* check all the range invariants */
+	AssertCheckRanges(ranges, cmpFn, ranges->colloid);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+}
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_regular_nulls = true;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float		a1 = PG_GETARG_FLOAT4(0);
+	float		a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double		a1 = PG_GETARG_FLOAT8(0);
+	double		a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16		a1 = PG_GETARG_INT16(0);
+	int16		a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32		a1 = PG_GETARG_INT32(0);
+	int32		a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64		a1 = PG_GETARG_INT64(0);
+	int64		a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double		da1,
+				da2;
+
+	ItemPointer pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	/*
+	 * We use the no-check variants here, because user-supplied values may
+	 * have (ip_posid == 0). See ItemPointerCompare.
+	 */
+	da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa1);
+
+	da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+		ItemPointerGetOffsetNumberNoCheck(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Computes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum		d;
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int			i;
+	float8		delta = 0;
+
+	Datum		a1 = PG_GETARG_DATUM(0);
+	Datum		a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be collapsed
+	 * (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN - 1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute approximate distance between two dates.
+ */
+Datum
+brin_minmax_multi_distance_date(PG_FUNCTION_ARGS)
+{
+	DateADT		dateVal1 = PG_GETARG_DATEADT(0);
+	DateADT		dateVal2 = PG_GETARG_DATEADT(1);
+
+	if (DATE_NOT_FINITE(dateVal1) || DATE_NOT_FINITE(dateVal2))
+		PG_RETURN_FLOAT8(0);
+
+	PG_RETURN_FLOAT8(dateVal1 - dateVal2);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeADT		ta = PG_GETARG_TIMEADT(0);
+	TimeADT		tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+Datum
+brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Timestamp	dt1 = PG_GETARG_TIMESTAMP(0);
+	Timestamp	dt2 = PG_GETARG_TIMESTAMP(1);
+
+	if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
+		PG_RETURN_FLOAT8(0);
+
+	delta = dt2 - dt1;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	Interval   *ia = PG_GETARG_INTERVAL_P(0);
+	Interval   *ib = PG_GETARG_INTERVAL_P(1);
+	Interval   *result;
+
+	result = (Interval *) palloc(sizeof(Interval));
+
+	result->month = ib->month - ia->month;
+	/* overflow check copied from int4mi */
+	if (!SAMESIGN(ib->month, ia->month) &&
+		!SAMESIGN(result->month, ib->month))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->day = ib->day - ia->day;
+	if (!SAMESIGN(ib->day, ia->day) &&
+		!SAMESIGN(result->day, ib->day))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	result->time = ib->time - ia->time;
+	if (!SAMESIGN(ib->time, ia->time) &&
+		!SAMESIGN(result->time, ib->time))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("interval out of range")));
+
+	/*
+	 * We assume months have 31 days - we don't need to be precise, in the
+	 * worst case we'll build somewhat less efficient ranges.
+	 */
+	delta = (float8) (result->month * 31 + result->day);
+
+	/* convert to microseconds (just like the time part) */
+	delta = 24L * 3600L * delta;
+
+	/* and add the time part */
+	delta += result->time / (float8) 1000000.0;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	float8		delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((float8) b->h - (float8) a->h);
+	delta /= 256;
+
+	delta += ((float8) b->g - (float8) a->g);
+	delta /= 256;
+
+	delta += ((float8) b->f - (float8) a->f);
+	delta /= 256;
+
+	delta += ((float8) b->e - (float8) a->e);
+	delta /= 256;
+
+	delta += ((float8) b->d - (float8) a->d);
+	delta /= 256;
+
+	delta += ((float8) b->c - (float8) a->c);
+	delta /= 256;
+
+	delta += ((float8) b->b - (float8) a->b);
+	delta /= 256;
+
+	delta += ((float8) b->a - (float8) a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from different families are considered to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	float8		delta;
+	int			i;
+	int			len;
+	unsigned char *addra,
+			   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be in
+	 * maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		PG_RETURN_FLOAT8(1.0);
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16;				/* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len - 1; i >= 0; i--)
+	{
+		delta += (float8) addrb[i] - (float8) addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+static void
+brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst)
+{
+	Ranges	   *ranges = (Ranges *) DatumGetPointer(src);
+	SerializedRanges *s;
+
+	/*
+	 * In batch mode, we need to compress the accumulated values to the
+	 * actually requested number of values/ranges.
+	 */
+	compactify_ranges(bdesc, ranges, ranges->target_maxvalues);
+
+	/* At this point everything has to be fully sorted. */
+	Assert(ranges->nsorted == ranges->nvalues);
+
+	s = range_serialize(ranges);
+	dst[0] = PointerGetDatum(s);
+}
+
+static int
+brin_minmax_multi_get_values(BrinDesc *bdesc, MinMaxMultiOptions *opts)
+{
+	return MinMaxMultiGetValuesPerRange(opts);
+}
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	MinMaxMultiOptions *opts = (MinMaxMultiOptions *) PG_GET_OPCLASS_OPTIONS();
+	Oid			colloid = PG_GET_COLLATION();
+	bool		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges	   *ranges;
+	SerializedRanges *serialized = NULL;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, if possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 *
+	 * When starting with an empty range, we assume this is a batch mode and
+	 * we use a larger buffer. The buffer size is derived from the BRIN range
+	 * size, number of rows per page, with some sensible min/max values. Small
+	 * buffer would be bad for performance, but large buffer might require a
+	 * lot of memory (because of keeping all the values).
+	 */
+	if (column->bv_allnulls)
+	{
+		MemoryContext oldctx;
+
+		int			target_maxvalues;
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		/* what was specified as a reloption? */
+		target_maxvalues = brin_minmax_multi_get_values(bdesc, opts);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, target_maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+		ranges = minmax_multi_init(maxvalues);
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+		ranges->target_maxvalues = target_maxvalues;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		MemoryContextSwitchTo(oldctx);
+
+		column->bv_allnulls = false;
+		modified = true;
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+	}
+	else if (!ranges)
+	{
+		MemoryContext oldctx;
+
+		int			maxvalues;
+		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+
+		oldctx = MemoryContextSwitchTo(column->bv_context);
+
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+		/*
+		 * Determine the insert buffer size - we use 10x the target, capped to
+		 * the maximum number of values in the heap range. This is more than
+		 * enough, considering the actual number of rows per page is likely
+		 * much lower, but meh.
+		 */
+		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+						MaxHeapTuplesPerPage * pagesPerRange);
+
+		/* but always at least the original value */
+		maxvalues = Max(maxvalues, serialized->maxvalues);
+
+		/* always cap by MIN/MAX */
+		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+		ranges = range_deserialize(maxvalues, serialized);
+
+		ranges->attno = attno;
+		ranges->colloid = colloid;
+		ranges->typid = attr->atttypid;
+
+		/* we'll certainly need the comparator, so just look it up now */
+		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+														 BTLessStrategyNumber);
+
+		column->bv_mem_value = PointerGetDatum(ranges);
+		column->bv_serialize = brin_minmax_multi_serialize;
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey    *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	SerializedRanges *serialized;
+	Ranges	   *ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	attno = column->bv_attno;
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized->maxvalues, serialized);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum		minval = ranges->values[2 * rangeno];
+		Datum		maxval = ranges->values[2 * rangeno + 1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+					{
+						Datum		compar;
+						FmgrInfo   *cmpFn;
+
+						/* by default this range does not match */
+						matches = false;
+
+						/*
+						 * Otherwise, need to compare the new value with
+						 * boundaries of all the ranges. First check if it's
+						 * less than the absolute minimum, which is the first
+						 * value in the array.
+						 */
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTLessStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+						/* smaller than the smallest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+																   BTGreaterStrategyNumber);
+						compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+						/* larger than the largest value in this range */
+						if (DatumGetBool(compar))
+							break;
+
+						/*
+						 * haven't managed to eliminate this range, so
+						 * consider it matching
+						 */
+						matches = true;
+
+						break;
+					}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/*
+	 * And now inspect the values. We don't bother with doing a binary search
+	 * here, because we're dealing with serialized / fully compacted ranges,
+	 * so there should be only very few values.
+	 */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum		val = ranges->values[2 * ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool		matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum		matches;
+			ScanKey		key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, val, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (!matching)
+				break;
+		}
+
+		/*
+		 * have we found a range matching all scan keys? if yes, we're done
+		 */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges *serialized_a;
+	SerializedRanges *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	ExpandedRange *eranges;
+	int			neranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue *distances;
+	MemoryContext ctx;
+	MemoryContext oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+	Assert(!col_a->bv_allnulls && !col_b->bv_allnulls);
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a);
+	ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	neranges = (ranges_a->nranges + ranges_a->nvalues) +
+		(ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise we'd
+	 * have problems e.g. when building indexes. So we create a local memory
+	 * context and make sure we free the memory before leaving this function
+	 * (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	eranges = (ExpandedRange *) palloc0(neranges * sizeof(ExpandedRange));
+
+	/* fill the expanded ranges with entries for the first range */
+	fill_expanded_ranges(eranges, ranges_a->nranges + ranges_a->nvalues,
+						 ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_expanded_ranges(&eranges[ranges_a->nranges + ranges_a->nvalues],
+						 ranges_b->nranges + ranges_b->nvalues,
+						 ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* sort the expanded ranges */
+	sort_expanded_ranges(cmpFn, colloid, eranges, neranges);
+
+	/*
+	 * We've loaded two different lists of expanded ranges, so some of them
+	 * may be overlapping. So walk through them and merge them.
+	 */
+	neranges = merge_overlapping_ranges(cmpFn, colloid, eranges, neranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertCheckExpandedRanges(bdesc, colloid, attno, attr, eranges, neranges);
+
+	/*
+	 * If needed, reduce some of the ranges.
+	 *
+	 * XXX This may be fairly expensive, so maybe we should do it only when
+	 * it's actually needed (when we have too many ranges).
+	 */
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, eranges, neranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many of them to get below the threshold. The
+	 * collapsed ranges will be stored as a single value.
+	 *
+	 * XXX This does not apply the load factor, as we don't expect to add more
+	 * values to the range, so we prefer to keep as many ranges as possible.
+	 *
+	 * XXX Can the maxvalues be different in the two ranges? Perhaps we should
+	 * use maximum of those?
+	 */
+	neranges = reduce_expanded_ranges(eranges, neranges, distances,
+									  ranges_a->maxvalues,
+									  cmpFn, colloid);
+
+	/* update the first range summary */
+	store_expanded_ranges(ranges_a, eranges, neranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it does not exist.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+								   uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
+
+Datum
+brin_minmax_multi_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(MinMaxMultiOptions));
+
+	add_local_int_reloption(relopts, "values_per_range", "desc",
+							MINMAX_MULTI_DEFAULT_VALUES_PER_PAGE, 8, 256,
+							offsetof(MinMaxMultiOptions, valuesPerRange));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * brin_minmax_multi_summary_in
+ *		- input routine for type brin_minmax_multi_summary.
+ *
+ * brin_minmax_multi_summary is only used internally to represent summaries
+ * in BRIN minmax-multi indexes, so it has no operations of its own, and we
+ * disallow input too.
+ */
+Datum
+brin_minmax_multi_summary_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * brin_minmax_multi_summary stores the data in binary form and parsing
+	 * text input is not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+
+/*
+ * brin_minmax_multi_summary_out
+ *		- output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized into a bytea value, but we
+ * want to output something nicer humans can understand.
+ */
+Datum
+brin_minmax_multi_summary_out(PG_FUNCTION_ARGS)
+{
+	int			i;
+	int			idx;
+	SerializedRanges *ranges;
+	Ranges	   *ranges_deserialized;
+	StringInfoData str;
+	bool		isvarlena;
+	Oid			outfunc;
+	FmgrInfo	fmgrinfo;
+	ArrayBuildState *astate_values = NULL;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	/*
+	 * Detoast to get value with full 4B header (can't be stored in a toast
+	 * table, but can use 1B header).
+	 */
+	ranges = (SerializedRanges *) PG_DETOAST_DATUM(PG_GETARG_BYTEA_PP(0));
+
+	/* lookup output func for the type */
+	getTypeOutputInfo(ranges->typid, &outfunc, &isvarlena);
+	fmgr_info(outfunc, &fmgrinfo);
+
+	/* deserialize the range info easy-to-process pieces */
+	ranges_deserialized = range_deserialize(ranges->maxvalues, ranges);
+
+	appendStringInfo(&str, "nranges: %u  nvalues: %u  maxvalues: %u",
+					 ranges_deserialized->nranges,
+					 ranges_deserialized->nvalues,
+					 ranges_deserialized->maxvalues);
+
+	/* serialize ranges */
+	idx = 0;
+	for (i = 0; i < ranges_deserialized->nranges; i++)
+	{
+		Datum		a,
+					b;
+		text	   *c;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+		b = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s ... %s",
+						 DatumGetPointer(a),
+						 DatumGetPointer(b));
+
+		c = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(c),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nranges > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " ranges: %s", extval);
+	}
+
+	/* serialize individual values */
+	astate_values = NULL;
+
+	for (i = 0; i < ranges_deserialized->nvalues; i++)
+	{
+		Datum		a;
+		text	   *b;
+		StringInfoData str;
+
+		initStringInfo(&str);
+
+		a = FunctionCall1(&fmgrinfo, ranges_deserialized->values[idx++]);
+
+		appendStringInfo(&str, "%s", DatumGetPointer(a));
+
+		b = cstring_to_text(str.data);
+
+		astate_values = accumArrayResult(astate_values,
+										 PointerGetDatum(b),
+										 false,
+										 TEXTOID,
+										 CurrentMemoryContext);
+	}
+
+	if (ranges_deserialized->nvalues > 0)
+	{
+		Oid			typoutput;
+		bool		typIsVarlena;
+		Datum		val;
+		char	   *extval;
+
+		getTypeOutputInfo(ANYARRAYOID, &typoutput, &typIsVarlena);
+
+		val = PointerGetDatum(makeArrayResult(astate_values, CurrentMemoryContext));
+
+		extval = OidOutputFunctionCall(typoutput, val);
+
+		appendStringInfo(&str, " values: %s", extval);
+	}
+
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * brin_minmax_multi_summary_recv
+ *		- binary input routine for type brin_minmax_multi_summary.
+ */
+Datum
+brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "brin_minmax_multi_summary")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * brin_minmax_multi_summary_send
+ *		- binary output routine for type brin_minmax_multi_summary.
+ *
+ * BRIN minmax-multi summaries are serialized in a bytea value (although
+ * the type is named differently), so let's just send that.
+ */
+Datum
+brin_minmax_multi_summary_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 8d03e609a3..baf87bdcae 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -159,6 +159,14 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 		if (tuple->bt_columns[keyno].bv_hasnulls)
 			anynulls = true;
 
+		/* If needed, serialize the values before forming the on-disk tuple. */
+		if (tuple->bt_columns[keyno].bv_serialize)
+		{
+			tuple->bt_columns[keyno].bv_serialize(brdesc,
+												  tuple->bt_columns[keyno].bv_mem_value,
+												  tuple->bt_columns[keyno].bv_values);
+		}
+
 		/*
 		 * Now obtain the values of each stored datum.  Note that some values
 		 * might be toasted, and we cannot rely on the original heap values
@@ -193,9 +201,9 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			 * If value is stored EXTERNAL, must fetch it so we are not
 			 * depending on outside storage.
 			 *
-			 * XXX Is this actually true? Could it be that the summary is
-			 * NULL even for range with non-NULL data? E.g. degenerate bloom
-			 * filter may be thrown away, etc.
+			 * XXX Is this actually true? Could it be that the summary is NULL
+			 * even for range with non-NULL data? E.g. degenerate bloom filter
+			 * may be thrown away, etc.
 			 */
 			if (VARATT_IS_EXTERNAL(DatumGetPointer(value)))
 			{
@@ -510,6 +518,11 @@ brin_memtuple_initialize(BrinMemTuple *dtuple, BrinDesc *brdesc)
 		dtuple->bt_columns[i].bv_allnulls = true;
 		dtuple->bt_columns[i].bv_hasnulls = false;
 		dtuple->bt_columns[i].bv_values = (Datum *) currdatum;
+
+		dtuple->bt_columns[i].bv_mem_value = PointerGetDatum(NULL);
+		dtuple->bt_columns[i].bv_serialize = NULL;
+		dtuple->bt_columns[i].bv_context = dtuple->bt_context;
+
 		currdatum += sizeof(Datum) * brdesc->bd_info[i]->oi_nstored;
 	}
 
@@ -589,6 +602,10 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple)
 
 		dtup->bt_columns[keyno].bv_hasnulls = hasnulls[keyno];
 		dtup->bt_columns[keyno].bv_allnulls = false;
+
+		dtup->bt_columns[keyno].bv_mem_value = PointerGetDatum(NULL);
+		dtup->bt_columns[keyno].bv_serialize = NULL;
+		dtup->bt_columns[keyno].bv_context = dtup->bt_context;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 5715bd58df..87de94f397 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -14,6 +14,11 @@
 #include "access/brin_internal.h"
 #include "access/tupdesc.h"
 
+/*
+ * The BRIN opclasses may register serialization callback, in case the on-disk
+ * and in-memory representations differ (e.g. for performance reasons).
+ */
+typedef void (*brin_serialize_callback_type) (BrinDesc *bdesc, Datum src, Datum *dst);
 
 /*
  * A BRIN index stores one index tuple per page range.  Each index tuple
@@ -27,6 +32,9 @@ typedef struct BrinValues
 	bool		bv_hasnulls;	/* are there any nulls in the page range? */
 	bool		bv_allnulls;	/* are all values nulls in the page range? */
 	Datum	   *bv_values;		/* current accumulated values */
+	Datum		bv_mem_value;	/* expanded accumulated values */
+	MemoryContext	bv_context;
+	brin_serialize_callback_type bv_serialize;
 } BrinValues;
 
 /*
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 82e874130d..2f7338ee82 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -161,18 +161,18 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  *		development purposes (such as in-progress patches and forks);
  *		they should not appear in released versions.
  *
- *		OIDs 10000-11999 are reserved for assignment by genbki.pl, for use
+ *		OIDs 10000-12999 are reserved for assignment by genbki.pl, for use
  *		when the .dat files in src/include/catalog/ do not specify an OID
  *		for a catalog entry that requires one.
  *
- *		OIDS 12000-16383 are reserved for assignment during initdb
- *		using the OID generator.  (We start the generator at 12000.)
+ *		OIDS 13000-16383 are reserved for assignment during initdb
+ *		using the OID generator.  (We start the generator at 13000.)
  *
  *		OIDs beginning at 16384 are assigned from the OID generator
  *		during normal multiuser operation.  (We force the generator up to
  *		16384 as soon as we are in normal operation.)
  *
- * The choices of 8000, 10000 and 12000 are completely arbitrary, and can be
+ * The choices of 8000, 10000 and 13000 are completely arbitrary, and can be
  * moved if we run low on OIDs in any category.  Changing the macros below,
  * and updating relevant documentation (see bki.sgml and RELEASE_CHANGES),
  * should be sufficient to do this.  Moving the 16384 boundary between
@@ -186,7 +186,7 @@ FullTransactionIdAdvance(FullTransactionId *dest)
  * ----------
  */
 #define FirstGenbkiObjectId		10000
-#define FirstBootstrapObjectId	12000
+#define FirstBootstrapObjectId	13000
 #define FirstNormalObjectId		16384
 
 /*
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 2f18734235..2db38232fe 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202103232
+#define CATALOG_VERSION_NO	202103233
 
 #endif
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 04d678f96a..8135854163 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -2009,6 +2009,152 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
+# minmax multi integer
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
+  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
+  amopmethod => 'brin' },
+
 # bloom integer
 
 { amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
@@ -2062,6 +2208,23 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
+# minmax multi oid
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
+  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
+  amopmethod => 'brin' },
+
 # bloom oid
 { amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
@@ -2088,6 +2251,22 @@
 { amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
   amopmethod => 'brin' },
+# minmax multi tid
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
+  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
+  amopmethod => 'brin' },
 
 # minmax float (float4, float8)
 
@@ -2155,6 +2334,72 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
+# minmax multi float (float4, float8)
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '2',
+  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '4',
+  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '2',
+  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '4',
+  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
+  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
+  amopmethod => 'brin' },
+
 # bloom float
 { amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
   amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
@@ -2180,6 +2425,23 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
+# minmax multi macaddr
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '1',
+  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '2',
+  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '3',
+  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '4',
+  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
+  amoprighttype => 'macaddr', amopstrategy => '5',
+  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
+
 # bloom macaddr
 { amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2202,6 +2464,23 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
+# minmax multi macaddr8
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '1',
+  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '2',
+  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '3',
+  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '4',
+  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
+{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
+  amoprighttype => 'macaddr8', amopstrategy => '5',
+  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
+
 # bloom macaddr8
 { amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2224,6 +2503,23 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
+# minmax multi inet
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
+  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
+  amopmethod => 'brin' },
+
 # bloom inet
 { amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
@@ -2288,6 +2584,23 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
+# minmax multi time without time zone
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
+  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
+  amopmethod => 'brin' },
+
 # bloom time without time zone
 { amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
@@ -2439,6 +2752,152 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
+  amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '1',
+  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '2',
+  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '3',
+  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '4',
+  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'date', amopstrategy => '5',
+  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamp', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
+
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '1',
+  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '2',
+  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '3',
+  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '4',
+  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
+  amoprighttype => 'timestamptz', amopstrategy => '5',
+  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
+
 # bloom datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
@@ -2470,6 +2929,23 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
+# minmax multi interval
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '1',
+  amopopr => '<(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '2',
+  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '3',
+  amopopr => '=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '4',
+  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
+{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
+  amoprighttype => 'interval', amopstrategy => '5',
+  amopopr => '>(interval,interval)', amopmethod => 'brin' },
+
 # bloom interval
 { amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2492,6 +2968,23 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
+# minmax multi time with time zone
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '2',
+  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '4',
+  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
+{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
+  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
+  amopmethod => 'brin' },
+
 # bloom time with time zone
 { amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
@@ -2548,6 +3041,23 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
+# minmax multi numeric
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '1',
+  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '2',
+  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '3',
+  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '4',
+  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
+{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
+  amoprighttype => 'numeric', amopstrategy => '5',
+  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
+
 # bloom numeric
 { amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
   amoprighttype => 'numeric', amopstrategy => '1',
@@ -2570,6 +3080,23 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
+# minmax multi uuid
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
+  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
+  amopmethod => 'brin' },
+
 # bloom uuid
 { amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
@@ -2636,6 +3163,23 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
+# minmax multi pg_lsn
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '2',
+  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '4',
+  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
+{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
+  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
+  amopmethod => 'brin' },
+
 # bloom pg_lsn
 { amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
   amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 2af8af3f4e..f2c0ccfa1f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -986,6 +986,152 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi integer: int2, int4, int8
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom integer: int2, int4, int8
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1081,6 +1227,23 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi oid
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
+
 # bloom oid
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
@@ -1127,6 +1290,23 @@
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
+# minmax multi tid
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
+
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1177,6 +1357,80 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi float
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float4' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float4', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_float8' },
+
 # bloom float
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1230,6 +1484,26 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr' },
+
 # bloom macaddr
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1264,6 +1538,26 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi macaddr8
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_macaddr8' },
+
 # bloom macaddr8
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1297,6 +1591,26 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi inet
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_inet' },
+
 # bloom inet
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1382,6 +1696,25 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
+# minmax multi time without time zone
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+
 # bloom time without time zone
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1507,6 +1840,170 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi datetime (date, timestamp, timestamptz)
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_time' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timestamp' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamp', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'timestamptz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_date' },
+
 # bloom datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1577,6 +2074,26 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi interval
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_interval' },
+
 # bloom interval
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1611,6 +2128,26 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi time with time zone
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_timetz' },
+
 # bloom time with time zone
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1671,6 +2208,26 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi numeric
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_numeric' },
+
 # bloom numeric
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '1',
@@ -1702,7 +2259,28 @@
   amprocrighttype => 'uuid', amprocnum => '3',
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_union' },
+
+# minmax multi uuid
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_uuid' },
 
 # bloom uuid
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
@@ -1759,6 +2337,26 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
+# minmax multi pg_lsn
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '1',
+  amproc => 'brin_minmax_multi_opcinfo' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '2',
+  amproc => 'brin_minmax_multi_add_value' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '3',
+  amproc => 'brin_minmax_multi_consistent' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '4',
+  amproc => 'brin_minmax_multi_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '5',
+  amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '11',
+  amproc => 'brin_minmax_multi_distance_pg_lsn' },
+
 # bloom pg_lsn
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6a5bb58baf..da25befefe 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -284,18 +284,27 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
+{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
+  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
+{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
+  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
+{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
+  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
+  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_bloom_ops',
   opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
@@ -307,6 +316,9 @@
   opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
+{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
+  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
+  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_bloom_ops',
   opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
@@ -315,33 +327,51 @@
 { opcmethod => 'brin', opcname => 'tid_bloom_ops',
   opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
   opcdefault => 'f'},
+{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
+  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
+  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
+{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
+  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
+{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
+  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
+  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_bloom_ops',
   opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
+{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
+  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
+  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
   opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
+{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
+  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
+  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
   opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
+{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
+  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
+  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_bloom_ops',
   opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
@@ -357,36 +387,54 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
+{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
+  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
+  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_bloom_ops',
   opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
+{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
+  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
+{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
+  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
+{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
+  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
+  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
   opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
+{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
+  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
+  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_bloom_ops',
   opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
+{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
+  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
+  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_bloom_ops',
   opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
@@ -398,6 +446,9 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
+{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
+  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
+  opckeytype => 'numeric', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'numeric_bloom_ops',
   opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
@@ -407,6 +458,9 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
+{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
+  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
+  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'uuid_bloom_ops',
   opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
@@ -416,6 +470,9 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
+{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
+  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
+  opckeytype => 'pg_lsn', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
   opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 7cc3d59a8c..04edca6cb0 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,10 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
+{ oid => '4602',
+  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '4572',
   opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
+{ oid => '4603',
+  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '4573',
@@ -194,10 +198,14 @@
   opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
+{ oid => '4604',
+  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '4575',
   opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
+{ oid => '4605',
+  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '4576',
   opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
@@ -214,26 +222,38 @@
   opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
+{ oid => '4606',
+  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '4580',
   opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
 { oid => '4581',
   opfmethod => 'brin', opfname => 'tid_bloom_ops' },
+{ oid => '4607',
+  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
+{ oid => '4608',
+  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '4582',
   opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
+{ oid => '4609',
+  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '4583',
   opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
+{ oid => '4610',
+  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '4584',
   opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
+{ oid => '4611',
+  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '4585',
@@ -244,10 +264,14 @@
   opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
+{ oid => '4612',
+  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '4587',
   opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
+{ oid => '4613',
+  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '4588',
   opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
@@ -256,12 +280,16 @@
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
+{ oid => '4614',
+  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '4589',
   opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
+{ oid => '4615',
+  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '4590',
   opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c89df733f7..183358c1ba 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8221,6 +8221,77 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
+# BRIN minmax multi
+{ oid => '4616', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
+{ oid => '4617', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_add_value' },
+{ oid => '4618', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
+  proargtypes => 'internal internal internal int4',
+  prosrc => 'brin_minmax_multi_consistent' },
+{ oid => '4619', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_union', prorettype => 'bool',
+  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
+{ oid => '4620', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
+  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
+
+{ oid => '4621', descr => 'BRIN multi minmax int2 distance',
+  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
+{ oid => '4622', descr => 'BRIN multi minmax int4 distance',
+  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
+{ oid => '4623', descr => 'BRIN multi minmax int8 distance',
+  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
+{ oid => '4624', descr => 'BRIN multi minmax float4 distance',
+  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
+{ oid => '4625', descr => 'BRIN multi minmax float8 distance',
+  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
+{ oid => '4626', descr => 'BRIN multi minmax numeric distance',
+  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '4627', descr => 'BRIN multi minmax tid distance',
+  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
+{ oid => '4628', descr => 'BRIN multi minmax uuid distance',
+  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
+{ oid => '4629', descr => 'BRIN multi minmax date distance',
+  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
+{ oid => '4630', descr => 'BRIN multi minmax time distance',
+  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
+{ oid => '4631', descr => 'BRIN multi minmax interval distance',
+  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
+{ oid => '4632', descr => 'BRIN multi minmax timetz distance',
+  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
+{ oid => '4633', descr => 'BRIN multi minmax pg_lsn distance',
+  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
+{ oid => '4634', descr => 'BRIN multi minmax macaddr distance',
+  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
+{ oid => '4635', descr => 'BRIN multi minmax macaddr8 distance',
+  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
+{ oid => '4636', descr => 'BRIN multi minmax inet distance',
+  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
+{ oid => '4637', descr => 'BRIN multi minmax timestamp distance',
+  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
+  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
   proname => 'brin_inclusion_opcinfo', prorettype => 'internal',
@@ -11445,4 +11516,18 @@
   proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
   proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
 
+{ oid => '4638', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
+  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
+{ oid => '4639', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
+{ oid => '4640', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
+  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
+  prosrc => 'brin_minmax_multi_summary_recv' },
+{ oid => '4641', descr => 'I/O',
+  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
+  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 2a82a3e544..8c145c00be 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -685,4 +685,10 @@
   typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
   typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
   typalign => 'i', typstorage => 'x', typcollation => 'default' },
+{ oid => '4601',
+  descr => 'BRIN minmax-multi summary',
+  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
+  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
+  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
+  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000000..0a7e4b8060
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,450 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+ERROR:  value 7 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+ERROR:  value 257 out of bounds for option "values_per_range"
+DETAIL:  Valid values are between "8" and "256".
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+DROP INDEX brinidx_multi;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- test building an index with many values, to force compaction of the buffer
+CREATE TABLE brin_large_range (a int4);
+INSERT INTO brin_large_range SELECT i FROM generate_series(1,10000) s(i);
+CREATE INDEX brin_large_range_idx ON brin_large_range USING brin (a int4_minmax_multi_ops);
+DROP TABLE brin_large_range;
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7a5b22d4f..76050af797 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,13 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                   List of operator classes
-  AM  | Input type | Storage type | Operator class | Default? 
-------+------------+--------------+----------------+----------
- brin | oid        |              | oid_bloom_ops  | no
- brin | oid        |              | oid_minmax_ops | yes
-(2 rows)
+                      List of operator classes
+  AM  | Input type | Storage type |    Operator class    | Default? 
+------+------------+--------------+----------------------+----------
+ brin | oid        |              | oid_bloom_ops        | no
+ brin | oid        |              | oid_minmax_multi_ops | no
+ brin | oid        |              | oid_minmax_ops       | yes
+(3 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 48ce3f7411..5480f979c6 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,15 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |        typname        
-------+-----------------------
+ oid  |           typname            
+------+------------------------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
  4600 | pg_brin_bloom_summary
+ 4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(5 rows)
+(6 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 95927a7bae..c6e49affeb 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom
+test: brin_bloom brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index c02a981c78..1d7eb418a7 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: namespace
 test: prepared_xacts
 test: brin
 test: brin_bloom
+test: brin_multi
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000000..f5ffcaea33
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,403 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+-- test minmax-multi specific index options
+-- number of values must be >= 16
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 7)
+);
+-- number of values must be <= 256
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops(values_per_range = 257)
+);
+
+-- first create an index with a single page range, to force compaction
+-- due to exceeding the number of values per summary
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+);
+
+DROP INDEX brinidx_multi;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- test building an index with many values, to force compaction of the buffer
+CREATE TABLE brin_large_range (a int4);
+INSERT INTO brin_large_range SELECT i FROM generate_series(1,10000) s(i);
+CREATE INDEX brin_large_range_idx ON brin_large_range USING brin (a int4_minmax_multi_ops);
+DROP TABLE brin_large_range;
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.30.2

0004-move-bloom-to-contrib-20210323.patchtext/x-patch; charset=UTF-8; name=0004-move-bloom-to-contrib-20210323.patchDownload
From c1ccd88fc2dc8c671191c141b4d99550a8624fe2 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Mon, 22 Mar 2021 03:22:41 +0100
Subject: [PATCH 4/5] move bloom to contrib

---
 contrib/Makefile                              |   1 +
 contrib/brin_bloom/.gitignore                 |   4 +
 contrib/brin_bloom/Makefile                   |  23 +
 contrib/brin_bloom/brin_bloom--1.0.sql        | 358 ++++++++++++++
 .../brin => contrib/brin_bloom}/brin_bloom.c  |  19 +-
 contrib/brin_bloom/brin_bloom.control         |   6 +
 .../brin_bloom}/expected/brin_bloom.out       |  21 +
 .../brin_bloom}/sql/brin_bloom.sql            |  25 +
 src/backend/access/brin/Makefile              |   1 -
 src/include/catalog/pg_amop.dat               | 115 -----
 src/include/catalog/pg_amproc.dat             | 447 ------------------
 src/include/catalog/pg_opclass.dat            |  72 ---
 src/include/catalog/pg_opfamily.dat           |  38 --
 src/include/catalog/pg_proc.dat               |  34 --
 src/include/catalog/pg_type.dat               |   6 -
 src/test/regress/expected/opr_sanity.out      |   3 +-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/expected/type_sanity.out     |   3 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 -
 20 files changed, 460 insertions(+), 722 deletions(-)
 create mode 100644 contrib/brin_bloom/.gitignore
 create mode 100644 contrib/brin_bloom/Makefile
 create mode 100644 contrib/brin_bloom/brin_bloom--1.0.sql
 rename {src/backend/access/brin => contrib/brin_bloom}/brin_bloom.c (97%)
 create mode 100644 contrib/brin_bloom/brin_bloom.control
 rename {src/test/regress => contrib/brin_bloom}/expected/brin_bloom.out (96%)
 rename {src/test/regress => contrib/brin_bloom}/sql/brin_bloom.sql (96%)

diff --git a/contrib/Makefile b/contrib/Makefile
index f27e458482..f878287429 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -12,6 +12,7 @@ SUBDIRS = \
 		bloom		\
 		btree_gin	\
 		btree_gist	\
+		brin_bloom	\
 		citext		\
 		cube		\
 		dblink		\
diff --git a/contrib/brin_bloom/.gitignore b/contrib/brin_bloom/.gitignore
new file mode 100644
index 0000000000..5dcb3ff972
--- /dev/null
+++ b/contrib/brin_bloom/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/brin_bloom/Makefile b/contrib/brin_bloom/Makefile
new file mode 100644
index 0000000000..d8ca383cf1
--- /dev/null
+++ b/contrib/brin_bloom/Makefile
@@ -0,0 +1,23 @@
+# contrib/brin_bloom/Makefile
+
+MODULE_big = brin_bloom
+OBJS = \
+	$(WIN32RES) \
+	brin_bloom.o
+
+EXTENSION = brin_bloom
+DATA = brin_bloom--1.0.sql
+PGFILEDESC = "brin_bloom - BRIN bloom operator classes"
+
+REGRESS = brin_bloom
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/brin_bloom
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/brin_bloom/brin_bloom--1.0.sql b/contrib/brin_bloom/brin_bloom--1.0.sql
new file mode 100644
index 0000000000..bc3e9ca35f
--- /dev/null
+++ b/contrib/brin_bloom/brin_bloom--1.0.sql
@@ -0,0 +1,358 @@
+/* contrib/brin_bloom/brin_bloom--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION brin_bloom" to load this file. \quit
+
+CREATE TYPE brin_bloom_summary;
+
+-- BRIN bloom summary data type
+CREATE FUNCTION brin_bloom_summary_in(cstring)
+RETURNS brin_bloom_summary
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_bloom_summary_out(brin_bloom_summary)
+RETURNS cstring
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_bloom_summary_recv(internal)
+RETURNS brin_bloom_summary
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_bloom_summary_send(brin_bloom_summary)
+RETURNS bytea
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE TYPE brin_bloom_summary (
+	INTERNALLENGTH = -1,
+	INPUT = brin_bloom_summary_in,
+	OUTPUT = brin_bloom_summary_out,
+	RECEIVE = brin_bloom_summary_recv,
+	SEND = brin_bloom_summary_send,
+	STORAGE = extended
+);
+
+-- BRIN support procedures
+CREATE FUNCTION brin_bloom_opcinfo(internal)
+RETURNS internal
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_bloom_add_value(internal, internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_bloom_consistent(internal, internal, internal, int4)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_bloom_union(internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_bloom_options(internal)
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE OPERATOR FAMILY datetime_bloom_ops USING brin;
+
+CREATE OPERATOR CLASS bpchar_bloom_ops
+FOR TYPE bpchar USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashbpchar(bpchar),
+STORAGE         bpchar;
+
+CREATE OPERATOR CLASS bytea_bloom_ops
+FOR TYPE bytea USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashvarlena(internal),
+STORAGE         bytea;
+
+CREATE OPERATOR CLASS char_bloom_ops
+FOR TYPE "char" USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashchar("char"),
+STORAGE         "char";
+
+CREATE OPERATOR CLASS date_bloom_ops
+FOR TYPE date USING brin FAMILY datetime_bloom_ops
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashint4(int4),
+STORAGE         date;
+
+CREATE OPERATOR FAMILY float_bloom_ops USING brin;
+
+CREATE OPERATOR CLASS float4_bloom_ops
+FOR TYPE float4 USING brin FAMILY float_bloom_ops
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashfloat4(float4),
+STORAGE         float4;
+
+CREATE OPERATOR CLASS float8_bloom_ops
+FOR TYPE float8 USING brin FAMILY float_bloom_ops
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashfloat8(float8),
+STORAGE         float8;
+
+CREATE OPERATOR FAMILY network_bloom_ops USING brin;
+
+CREATE OPERATOR CLASS inet_bloom_ops
+FOR TYPE inet USING brin FAMILY network_bloom_ops
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashinet(inet),
+STORAGE         inet;
+
+CREATE OPERATOR FAMILY integer_bloom_ops USING brin;
+
+CREATE OPERATOR CLASS int2_bloom_ops
+FOR TYPE int2 USING brin FAMILY integer_bloom_ops
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashint2(int2),
+STORAGE         int2;
+
+CREATE OPERATOR CLASS int4_bloom_ops
+FOR TYPE int4 USING brin FAMILY integer_bloom_ops
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashint4(int4),
+STORAGE         int4;
+
+CREATE OPERATOR CLASS int8_bloom_ops
+FOR TYPE int8 USING brin FAMILY integer_bloom_ops
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashint8(int8),
+STORAGE         int8;
+
+CREATE OPERATOR CLASS interval_bloom_ops
+FOR TYPE interval USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      interval_hash(interval),
+STORAGE         interval;
+
+CREATE OPERATOR CLASS macaddr_bloom_ops
+FOR TYPE macaddr USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashmacaddr(macaddr),
+STORAGE         macaddr;
+
+CREATE OPERATOR CLASS macaddr8_bloom_ops
+FOR TYPE macaddr8 USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashmacaddr8(macaddr8),
+STORAGE         macaddr8;
+
+CREATE OPERATOR CLASS name_bloom_ops
+FOR TYPE name USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashname(name),
+STORAGE         name;
+
+CREATE OPERATOR CLASS numeric_bloom_ops
+FOR TYPE numeric USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hash_numeric(numeric),
+STORAGE         numeric;
+
+CREATE OPERATOR CLASS oid_bloom_ops
+FOR TYPE oid USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashoid(oid),
+STORAGE         oid;
+
+CREATE OPERATOR CLASS pg_lsn_bloom_ops
+FOR TYPE pg_lsn USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      pg_lsn_hash(pg_lsn),
+STORAGE         pg_lsn;
+
+CREATE OPERATOR CLASS text_bloom_ops
+FOR TYPE text USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashtext(text),
+STORAGE         text;
+
+CREATE OPERATOR CLASS tid_bloom_ops
+FOR TYPE tid USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      hashtid(tid),
+STORAGE         tid;
+
+CREATE OPERATOR CLASS time_bloom_ops
+FOR TYPE time USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      time_hash(time),
+STORAGE         time;
+
+CREATE OPERATOR CLASS timestamp_bloom_ops
+FOR TYPE timestamp USING brin FAMILY datetime_bloom_ops
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      timestamp_hash(timestamp),
+STORAGE         timestamp;
+
+CREATE OPERATOR CLASS timestamptz_bloom_ops
+FOR TYPE timestamptz USING brin FAMILY datetime_bloom_ops
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      timestamp_hash(timestamp),
+STORAGE         timestamptz;
+
+CREATE OPERATOR CLASS timetz_bloom_ops
+FOR TYPE timetz USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      timetz_hash(timetz),
+STORAGE         timetz;
+
+CREATE OPERATOR CLASS uuid_bloom_ops
+FOR TYPE uuid USING brin
+AS
+    OPERATOR        1       =,
+    FUNCTION        1       brin_bloom_opcinfo(internal),
+    FUNCTION        2       brin_bloom_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_bloom_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_bloom_union(internal, internal, internal),
+    FUNCTION        5       brin_bloom_options(internal),
+    FUNCTION        11      uuid_hash(uuid),
+STORAGE         uuid;
diff --git a/src/backend/access/brin/brin_bloom.c b/contrib/brin_bloom/brin_bloom.c
similarity index 97%
rename from src/backend/access/brin/brin_bloom.c
rename to contrib/brin_bloom/brin_bloom.c
index 2214fb4d0c..51e96ed1ef 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/contrib/brin_bloom/brin_bloom.c
@@ -123,6 +123,7 @@
 #include "access/htup_details.h"
 #include "access/reloptions.h"
 #include "access/stratnum.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_amop.h"
 #include "utils/builtins.h"
@@ -133,6 +134,9 @@
 
 #include <math.h>
 
+PG_MODULE_MAGIC;
+
+
 #define BloomEqualStrategyNumber	1
 
 /*
@@ -225,6 +229,18 @@ typedef struct BloomOptions
 #define BLOOM_SEED_1	0x71d924af
 #define BLOOM_SEED_2	0xba48b314
 
+
+PG_FUNCTION_INFO_V1(brin_bloom_summary_in);
+PG_FUNCTION_INFO_V1(brin_bloom_summary_out);
+PG_FUNCTION_INFO_V1(brin_bloom_summary_send);
+PG_FUNCTION_INFO_V1(brin_bloom_summary_recv);
+
+PG_FUNCTION_INFO_V1(brin_bloom_opcinfo);
+PG_FUNCTION_INFO_V1(brin_bloom_options);
+PG_FUNCTION_INFO_V1(brin_bloom_add_value);
+PG_FUNCTION_INFO_V1(brin_bloom_consistent);
+PG_FUNCTION_INFO_V1(brin_bloom_union);
+
 /*
  * Bloom Filter
  *
@@ -424,6 +440,7 @@ Datum
 brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 {
 	BrinOpcInfo *result;
+	Oid	typoid = TypenameGetTypidExtended("brin_bloom_summary", false);
 
 	/*
 	 * opaque->strategy_procinfos is initialized lazily; here it is set to
@@ -438,7 +455,7 @@ brin_bloom_opcinfo(PG_FUNCTION_ARGS)
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (BloomOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
-	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_BLOOM_SUMMARYOID, 0);
+	result->oi_typcache[0] = lookup_type_cache(typoid, 0);
 
 	PG_RETURN_POINTER(result);
 }
diff --git a/contrib/brin_bloom/brin_bloom.control b/contrib/brin_bloom/brin_bloom.control
new file mode 100644
index 0000000000..b77048a4ac
--- /dev/null
+++ b/contrib/brin_bloom/brin_bloom.control
@@ -0,0 +1,6 @@
+# brin_bloom extension
+comment = 'support for BRIN bloom indexes'
+default_version = '1.0'
+module_pathname = '$libdir/brin_bloom'
+relocatable = true
+trusted = true
diff --git a/src/test/regress/expected/brin_bloom.out b/contrib/brin_bloom/expected/brin_bloom.out
similarity index 96%
rename from src/test/regress/expected/brin_bloom.out
rename to contrib/brin_bloom/expected/brin_bloom.out
index 32c56a996a..4544d09691 100644
--- a/src/test/regress/expected/brin_bloom.out
+++ b/contrib/brin_bloom/expected/brin_bloom.out
@@ -1,3 +1,24 @@
+CREATE EXTENSION brin_bloom;
+CREATE TABLE tenk1 (
+	unique1		int4,
+	unique2		int4,
+	two			int4,
+	four		int4,
+	ten			int4,
+	twenty		int4,
+	hundred		int4,
+	thousand	int4,
+	twothousand	int4,
+	fivethous	int4,
+	tenthous	int4,
+	odd			int4,
+	even		int4,
+	stringu1	name,
+	stringu2	name,
+	string4		name
+);
+\copy tenk1 from '../../src/test/regress/data/tenk.data'
+CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops);
 CREATE TABLE brintest_bloom (byteacol bytea,
 	charcol "char",
 	namecol name,
diff --git a/src/test/regress/sql/brin_bloom.sql b/contrib/brin_bloom/sql/brin_bloom.sql
similarity index 96%
rename from src/test/regress/sql/brin_bloom.sql
rename to contrib/brin_bloom/sql/brin_bloom.sql
index 5d499208e3..fa79053ac5 100644
--- a/src/test/regress/sql/brin_bloom.sql
+++ b/contrib/brin_bloom/sql/brin_bloom.sql
@@ -1,3 +1,28 @@
+CREATE EXTENSION brin_bloom;
+
+CREATE TABLE tenk1 (
+	unique1		int4,
+	unique2		int4,
+	two			int4,
+	four		int4,
+	ten			int4,
+	twenty		int4,
+	hundred		int4,
+	thousand	int4,
+	twothousand	int4,
+	fivethous	int4,
+	tenthous	int4,
+	odd			int4,
+	even		int4,
+	stringu1	name,
+	stringu2	name,
+	string4		name
+);
+
+\copy tenk1 from '../../src/test/regress/data/tenk.data'
+
+CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops);
+
 CREATE TABLE brintest_bloom (byteacol bytea,
 	charcol "char",
 	namecol name,
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index a386cb71f1..75eb87ec10 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -14,7 +14,6 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	brin.o \
-	brin_bloom.o \
 	brin_inclusion.o \
 	brin_minmax.o \
 	brin_minmax_multi.o \
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 8135854163..51bef55a57 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1814,11 +1814,6 @@
   amoprighttype => 'bytea', amopstrategy => '5', amopopr => '>(bytea,bytea)',
   amopmethod => 'brin' },
 
-# bloom bytea
-{ amopfamily => 'brin/bytea_bloom_ops', amoplefttype => 'bytea',
-  amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
-  amopmethod => 'brin' },
-
 # minmax "char"
 { amopfamily => 'brin/char_minmax_ops', amoplefttype => 'char',
   amoprighttype => 'char', amopstrategy => '1', amopopr => '<(char,char)',
@@ -1836,11 +1831,6 @@
   amoprighttype => 'char', amopstrategy => '5', amopopr => '>(char,char)',
   amopmethod => 'brin' },
 
-# bloom "char"
-{ amopfamily => 'brin/char_bloom_ops', amoplefttype => 'char',
-  amoprighttype => 'char', amopstrategy => '1', amopopr => '=(char,char)',
-  amopmethod => 'brin' },
-
 # minmax name
 { amopfamily => 'brin/name_minmax_ops', amoplefttype => 'name',
   amoprighttype => 'name', amopstrategy => '1', amopopr => '<(name,name)',
@@ -1858,11 +1848,6 @@
   amoprighttype => 'name', amopstrategy => '5', amopopr => '>(name,name)',
   amopmethod => 'brin' },
 
-# bloom name
-{ amopfamily => 'brin/name_bloom_ops', amoplefttype => 'name',
-  amoprighttype => 'name', amopstrategy => '1', amopopr => '=(name,name)',
-  amopmethod => 'brin' },
-
 # minmax integer
 
 { amopfamily => 'brin/integer_minmax_ops', amoplefttype => 'int8',
@@ -2155,20 +2140,6 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
-# bloom integer
-
-{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '1', amopopr => '=(int8,int8)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '1', amopopr => '=(int2,int2)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_bloom_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '1', amopopr => '=(int4,int4)',
-  amopmethod => 'brin' },
-
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2186,11 +2157,6 @@
   amoprighttype => 'text', amopstrategy => '5', amopopr => '>(text,text)',
   amopmethod => 'brin' },
 
-# bloom text
-{ amopfamily => 'brin/text_bloom_ops', amoplefttype => 'text',
-  amoprighttype => 'text', amopstrategy => '1', amopopr => '=(text,text)',
-  amopmethod => 'brin' },
-
 # minmax oid
 { amopfamily => 'brin/oid_minmax_ops', amoplefttype => 'oid',
   amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
@@ -2225,11 +2191,6 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
-# bloom oid
-{ amopfamily => 'brin/oid_bloom_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)',
-  amopmethod => 'brin' },
-
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2247,10 +2208,6 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
-# tid oid
-{ amopfamily => 'brin/tid_bloom_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '1', amopopr => '=(tid,tid)',
-  amopmethod => 'brin' },
 # minmax multi tid
 { amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2400,14 +2357,6 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
-# bloom float
-{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '1', amopopr => '=(float4,float4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_bloom_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '1', amopopr => '=(float8,float8)',
-  amopmethod => 'brin' },
-
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2442,11 +2391,6 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
-# bloom macaddr
-{ amopfamily => 'brin/macaddr_bloom_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '1',
-  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
-
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2481,11 +2425,6 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
-# bloom macaddr8
-{ amopfamily => 'brin/macaddr8_bloom_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '1',
-  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
-
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2520,11 +2459,6 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
-# bloom inet
-{ amopfamily => 'brin/network_bloom_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '1', amopopr => '=(inet,inet)',
-  amopmethod => 'brin' },
-
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2562,11 +2496,6 @@
   amoprighttype => 'bpchar', amopstrategy => '5', amopopr => '>(bpchar,bpchar)',
   amopmethod => 'brin' },
 
-# bloom character
-{ amopfamily => 'brin/bpchar_bloom_ops', amoplefttype => 'bpchar',
-  amoprighttype => 'bpchar', amopstrategy => '1', amopopr => '=(bpchar,bpchar)',
-  amopmethod => 'brin' },
-
 # minmax time without time zone
 { amopfamily => 'brin/time_minmax_ops', amoplefttype => 'time',
   amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
@@ -2601,11 +2530,6 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
-# bloom time without time zone
-{ amopfamily => 'brin/time_bloom_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '1', amopopr => '=(time,time)',
-  amopmethod => 'brin' },
-
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2898,20 +2822,6 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
-# bloom datetime (date, timestamp, timestamptz)
-
-{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '1',
-  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '1', amopopr => '=(date,date)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_bloom_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '1',
-  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
-
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2946,11 +2856,6 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
-# bloom interval
-{ amopfamily => 'brin/interval_bloom_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '1',
-  amopopr => '=(interval,interval)', amopmethod => 'brin' },
-
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2985,11 +2890,6 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
-# bloom time with time zone
-{ amopfamily => 'brin/timetz_bloom_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '=(timetz,timetz)',
-  amopmethod => 'brin' },
-
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -3058,11 +2958,6 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
-# bloom numeric
-{ amopfamily => 'brin/numeric_bloom_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '1',
-  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
-
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -3097,11 +2992,6 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
-# bloom uuid
-{ amopfamily => 'brin/uuid_bloom_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '=(uuid,uuid)',
-  amopmethod => 'brin' },
-
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -3180,11 +3070,6 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
-# bloom pg_lsn
-{ amopfamily => 'brin/pg_lsn_bloom_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '=(pg_lsn,pg_lsn)',
-  amopmethod => 'brin' },
-
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index f2c0ccfa1f..68c3342eeb 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -805,24 +805,6 @@
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# bloom bytea
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
-  amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashvarlena' },
-
 # minmax "char"
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '1',
@@ -836,24 +818,6 @@
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# bloom "char"
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
-
 # minmax name
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '1',
@@ -867,24 +831,6 @@
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# bloom name
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
-  amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
-
 # minmax integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '1',
@@ -1132,58 +1078,6 @@
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
 
-# bloom integer: int2, int4, int8
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
-
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
-
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
-
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -1197,24 +1091,6 @@
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# bloom text
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
-  amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
-
 # minmax oid
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1244,23 +1120,6 @@
 { amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
 
-# bloom oid
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
-
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1273,23 +1132,6 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# bloom tid
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
-
 # minmax multi tid
 { amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
@@ -1431,45 +1273,6 @@
   amprocrighttype => 'float4', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_float8' },
 
-# bloom float
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '11',
-  amproc => 'hashfloat4' },
-
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '11',
-  amproc => 'hashfloat8' },
-
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1504,26 +1307,6 @@
   amprocrighttype => 'macaddr', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_macaddr' },
 
-# bloom macaddr
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '11',
-  amproc => 'hashmacaddr' },
-
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1558,26 +1341,6 @@
   amprocrighttype => 'macaddr8', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_macaddr8' },
 
-# bloom macaddr8
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '11',
-  amproc => 'hashmacaddr8' },
-
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1611,24 +1374,6 @@
   amprocrighttype => 'inet', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_inet' },
 
-# bloom inet
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
-
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1663,26 +1408,6 @@
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# bloom character
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '11',
-  amproc => 'hashbpchar' },
-
 # minmax time without time zone
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '1',
@@ -1715,24 +1440,6 @@
   amprocrighttype => 'time', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_time' },
 
-# bloom time without time zone
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
-
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -2004,62 +1711,6 @@
   amprocrighttype => 'timestamptz', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_date' },
 
-# bloom datetime (date, timestamp, timestamptz)
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '11',
-  amproc => 'timestamp_hash' },
-
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '11',
-  amproc => 'timestamp_hash' },
-
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
-
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -2094,26 +1745,6 @@
   amprocrighttype => 'interval', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_interval' },
 
-# bloom interval
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '11',
-  amproc => 'interval_hash' },
-
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -2148,26 +1779,6 @@
   amprocrighttype => 'timetz', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_timetz' },
 
-# bloom time with time zone
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '11',
-  amproc => 'timetz_hash' },
-
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -2228,26 +1839,6 @@
   amprocrighttype => 'numeric', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_numeric' },
 
-# bloom numeric
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '11',
-  amproc => 'hash_numeric' },
-
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -2282,24 +1873,6 @@
   amprocrighttype => 'uuid', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_uuid' },
 
-# bloom uuid
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
-
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -2357,26 +1930,6 @@
   amprocrighttype => 'pg_lsn', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_pg_lsn' },
 
-# bloom pg_lsn
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '1',
-  amproc => 'brin_bloom_opcinfo' },
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '2',
-  amproc => 'brin_bloom_add_value' },
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '3',
-  amproc => 'brin_bloom_consistent' },
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '4',
-  amproc => 'brin_bloom_union' },
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '5',
-  amproc => 'brin_bloom_options' },
-{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '11',
-  amproc => 'pg_lsn_hash' },
-
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index da25befefe..e510e28655 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -266,67 +266,40 @@
 { opcmethod => 'brin', opcname => 'bytea_minmax_ops',
   opcfamily => 'brin/bytea_minmax_ops', opcintype => 'bytea',
   opckeytype => 'bytea' },
-{ opcmethod => 'brin', opcname => 'bytea_bloom_ops',
-  opcfamily => 'brin/bytea_bloom_ops', opcintype => 'bytea',
-  opckeytype => 'bytea', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'char_minmax_ops',
   opcfamily => 'brin/char_minmax_ops', opcintype => 'char',
   opckeytype => 'char' },
-{ opcmethod => 'brin', opcname => 'char_bloom_ops',
-  opcfamily => 'brin/char_bloom_ops', opcintype => 'char',
-  opckeytype => 'char', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'name_minmax_ops',
   opcfamily => 'brin/name_minmax_ops', opcintype => 'name',
   opckeytype => 'name' },
-{ opcmethod => 'brin', opcname => 'name_bloom_ops',
-  opcfamily => 'brin/name_bloom_ops', opcintype => 'name',
-  opckeytype => 'name', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
 { opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
   opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
   opckeytype => 'int8', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'int8_bloom_ops',
-  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int8',
-  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
 { opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
   opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
   opckeytype => 'int2', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'int2_bloom_ops',
-  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int2',
-  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
 { opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
   opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
   opckeytype => 'int4', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'int4_bloom_ops',
-  opcfamily => 'brin/integer_bloom_ops', opcintype => 'int4',
-  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
-{ opcmethod => 'brin', opcname => 'text_bloom_ops',
-  opcfamily => 'brin/text_bloom_ops', opcintype => 'text',
-  opckeytype => 'text', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
 { opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
   opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
   opckeytype => 'oid', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'oid_bloom_ops',
-  opcfamily => 'brin/oid_bloom_ops', opcintype => 'oid',
-  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
-{ opcmethod => 'brin', opcname => 'tid_bloom_ops',
-  opcfamily => 'brin/tid_bloom_ops', opcintype => 'tid', opckeytype => 'tid',
-  opcdefault => 'f'},
 { opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
   opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
   opckeytype => 'tid', opcdefault => 'f' },
@@ -336,108 +309,72 @@
 { opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
   opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
   opckeytype => 'float4', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'float4_bloom_ops',
-  opcfamily => 'brin/float_bloom_ops', opcintype => 'float4',
-  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
 { opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
   opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
   opckeytype => 'float8', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'float8_bloom_ops',
-  opcfamily => 'brin/float_bloom_ops', opcintype => 'float8',
-  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
   opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'macaddr_bloom_ops',
-  opcfamily => 'brin/macaddr_bloom_ops', opcintype => 'macaddr',
-  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
   opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'macaddr8_bloom_ops',
-  opcfamily => 'brin/macaddr8_bloom_ops', opcintype => 'macaddr8',
-  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
   opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'inet_bloom_ops',
-  opcfamily => 'brin/network_bloom_ops', opcintype => 'inet',
-  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
 { opcmethod => 'brin', opcname => 'bpchar_minmax_ops',
   opcfamily => 'brin/bpchar_minmax_ops', opcintype => 'bpchar',
   opckeytype => 'bpchar' },
-{ opcmethod => 'brin', opcname => 'bpchar_bloom_ops',
-  opcfamily => 'brin/bpchar_bloom_ops', opcintype => 'bpchar',
-  opckeytype => 'bpchar', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
 { opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
   opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
   opckeytype => 'time', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'time_bloom_ops',
-  opcfamily => 'brin/time_bloom_ops', opcintype => 'time',
-  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
 { opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
   opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
   opckeytype => 'date', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'date_bloom_ops',
-  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'date',
-  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
   opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'timestamp_bloom_ops',
-  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamp',
-  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
   opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'timestamptz_bloom_ops',
-  opcfamily => 'brin/datetime_bloom_ops', opcintype => 'timestamptz',
-  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
 { opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
   opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
   opckeytype => 'interval', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'interval_bloom_ops',
-  opcfamily => 'brin/interval_bloom_ops', opcintype => 'interval',
-  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
   opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
   opckeytype => 'timetz', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'timetz_bloom_ops',
-  opcfamily => 'brin/timetz_bloom_ops', opcintype => 'timetz',
-  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -449,9 +386,6 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
   opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
   opckeytype => 'numeric', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'numeric_bloom_ops',
-  opcfamily => 'brin/numeric_bloom_ops', opcintype => 'numeric',
-  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
@@ -461,9 +395,6 @@
 { opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
   opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
   opckeytype => 'uuid', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'uuid_bloom_ops',
-  opcfamily => 'brin/uuid_bloom_ops', opcintype => 'uuid',
-  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
@@ -473,9 +404,6 @@
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
   opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn', opcdefault => 'f' },
-{ opcmethod => 'brin', opcname => 'pg_lsn_bloom_ops',
-  opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn',
-  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 04edca6cb0..6218eade08 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -184,96 +184,62 @@
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
 { oid => '4602',
   opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
-{ oid => '4572',
-  opfmethod => 'brin', opfname => 'integer_bloom_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
 { oid => '4603',
   opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
-{ oid => '4573',
-  opfmethod => 'brin', opfname => 'text_bloom_ops' },
-{ oid => '4574',
-  opfmethod => 'brin', opfname => 'numeric_bloom_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
 { oid => '4604',
   opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
-{ oid => '4575',
-  opfmethod => 'brin', opfname => 'timetz_bloom_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
 { oid => '4605',
   opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
-{ oid => '4576',
-  opfmethod => 'brin', opfname => 'datetime_bloom_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
-{ oid => '4577',
-  opfmethod => 'brin', opfname => 'char_bloom_ops' },
 { oid => '4064',
   opfmethod => 'brin', opfname => 'bytea_minmax_ops' },
-{ oid => '4578',
-  opfmethod => 'brin', opfname => 'bytea_bloom_ops' },
 { oid => '4065',
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
-{ oid => '4579',
-  opfmethod => 'brin', opfname => 'name_bloom_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
 { oid => '4606',
   opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
-{ oid => '4580',
-  opfmethod => 'brin', opfname => 'oid_bloom_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
-{ oid => '4581',
-  opfmethod => 'brin', opfname => 'tid_bloom_ops' },
 { oid => '4607',
   opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
 { oid => '4608',
   opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
-{ oid => '4582',
-  opfmethod => 'brin', opfname => 'float_bloom_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
 { oid => '4609',
   opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
-{ oid => '4583',
-  opfmethod => 'brin', opfname => 'macaddr_bloom_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
 { oid => '4610',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
-{ oid => '4584',
-  opfmethod => 'brin', opfname => 'macaddr8_bloom_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
 { oid => '4611',
   opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
-{ oid => '4585',
-  opfmethod => 'brin', opfname => 'network_bloom_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
-{ oid => '4586',
-  opfmethod => 'brin', opfname => 'bpchar_bloom_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
 { oid => '4612',
   opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
-{ oid => '4587',
-  opfmethod => 'brin', opfname => 'time_bloom_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
 { oid => '4613',
   opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
-{ oid => '4588',
-  opfmethod => 'brin', opfname => 'interval_bloom_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
@@ -282,16 +248,12 @@
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
 { oid => '4614',
   opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
-{ oid => '4589',
-  opfmethod => 'brin', opfname => 'uuid_bloom_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
 { oid => '4615',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
-{ oid => '4590',
-  opfmethod => 'brin', opfname => 'pg_lsn_bloom_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 183358c1ba..cb7d421d5a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8309,26 +8309,6 @@
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
 
-# BRIN bloom
-{ oid => '4591', descr => 'BRIN bloom support',
-  proname => 'brin_bloom_opcinfo', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'brin_bloom_opcinfo' },
-{ oid => '4592', descr => 'BRIN bloom support',
-  proname => 'brin_bloom_add_value', prorettype => 'bool',
-  proargtypes => 'internal internal internal internal',
-  prosrc => 'brin_bloom_add_value' },
-{ oid => '4593', descr => 'BRIN bloom support',
-  proname => 'brin_bloom_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal int4',
-  prosrc => 'brin_bloom_consistent' },
-{ oid => '4594', descr => 'BRIN bloom support',
-  proname => 'brin_bloom_union', prorettype => 'bool',
-  proargtypes => 'internal internal internal',
-  prosrc => 'brin_bloom_union' },
-{ oid => '4595', descr => 'BRIN bloom support',
-  proname => 'brin_bloom_options', prorettype => 'void', proisstrict => 'f',
-  proargtypes => 'internal', prosrc => 'brin_bloom_options' },
-
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
   proname => 'pg_advisory_lock', provolatile => 'v', proparallel => 'r',
@@ -11502,20 +11482,6 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
-{ oid => '4596', descr => 'I/O',
-  proname => 'brin_bloom_summary_in', prorettype => 'pg_brin_bloom_summary',
-  proargtypes => 'cstring', prosrc => 'brin_bloom_summary_in' },
-{ oid => '4597', descr => 'I/O',
-  proname => 'brin_bloom_summary_out', prorettype => 'cstring',
-  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_out' },
-{ oid => '4598', descr => 'I/O',
-  proname => 'brin_bloom_summary_recv', provolatile => 's',
-  prorettype => 'pg_brin_bloom_summary', proargtypes => 'internal',
-  prosrc => 'brin_bloom_summary_recv' },
-{ oid => '4599', descr => 'I/O',
-  proname => 'brin_bloom_summary_send', provolatile => 's', prorettype => 'bytea',
-  proargtypes => 'pg_brin_bloom_summary', prosrc => 'brin_bloom_summary_send' },
-
 { oid => '4638', descr => 'I/O',
   proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
   proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 8c145c00be..387d25d66a 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,12 +679,6 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-{ oid => '4600',
-  descr => 'BRIN bloom summary',
-  typname => 'pg_brin_bloom_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
-  typinput => 'brin_bloom_summary_in', typoutput => 'brin_bloom_summary_out',
-  typreceive => 'brin_bloom_summary_recv', typsend => 'brin_bloom_summary_send',
-  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 { oid => '4601',
   descr => 'BRIN minmax-multi summary',
   typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index ef4b4444b9..254ca06d3d 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2037,7 +2037,6 @@ ORDER BY 1, 2, 3;
        2742 |           16 | @@
        3580 |            1 | <
        3580 |            1 | <<
-       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -2101,7 +2100,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(124 rows)
+(123 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 76050af797..ee45e37b84 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4993,10 +4993,9 @@ List of access methods
                       List of operator classes
   AM  | Input type | Storage type |    Operator class    | Default? 
 ------+------------+--------------+----------------------+----------
- brin | oid        |              | oid_bloom_ops        | no
  brin | oid        |              | oid_minmax_multi_ops | no
  brin | oid        |              | oid_minmax_ops       | yes
-(3 rows)
+(2 rows)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 5480f979c6..26f4b6097b 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -72,10 +72,9 @@ ORDER BY p1.oid;
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
- 4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+(5 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index c6e49affeb..1c6bbfcf54 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -80,7 +80,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom brin_multi
+test: brin_multi
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 1d7eb418a7..dd3f3a9f0b 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,7 +108,6 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
-test: brin_bloom
 test: brin_multi
 test: gin
 test: gist
-- 
2.30.2

0005-move-minmax-multi-to-contrib-20210323.patchtext/x-patch; charset=UTF-8; name=0005-move-minmax-multi-to-contrib-20210323.patchDownload
From 32bdd062ed87ca43102aaf360c126874af14a036 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Mon, 22 Mar 2021 05:10:37 +0100
Subject: [PATCH 5/5] move minmax-multi to contrib

---
 contrib/Makefile                              |   1 +
 contrib/brin_minmax_multi/.gitignore          |   4 +
 contrib/brin_minmax_multi/Makefile            |  23 +
 .../brin_minmax_multi--1.0.sql                | 552 ++++++++++++++++
 .../brin_minmax_multi}/brin_minmax_multi.c    |  63 +-
 .../brin_minmax_multi.control                 |   6 +
 .../expected/brin_multi.out                   |  21 +
 .../brin_minmax_multi}/sql/brin_multi.sql     |  25 +
 src/backend/access/brin/Makefile              |   1 -
 src/include/catalog/pg_amop.dat               | 545 ----------------
 src/include/catalog/pg_amproc.dat             | 597 ------------------
 src/include/catalog/pg_opclass.dat            |  57 --
 src/include/catalog/pg_opfamily.dat           |  28 -
 src/include/catalog/pg_proc.dat               |  85 +--
 src/include/catalog/pg_type.dat               |   6 -
 src/test/regress/expected/psql.out            |  11 +-
 src/test/regress/expected/type_sanity.out     |   7 +-
 src/test/regress/parallel_schedule            |   5 -
 src/test/regress/serial_schedule              |   1 -
 19 files changed, 703 insertions(+), 1335 deletions(-)
 create mode 100644 contrib/brin_minmax_multi/.gitignore
 create mode 100644 contrib/brin_minmax_multi/Makefile
 create mode 100644 contrib/brin_minmax_multi/brin_minmax_multi--1.0.sql
 rename {src/backend/access/brin => contrib/brin_minmax_multi}/brin_minmax_multi.c (96%)
 create mode 100644 contrib/brin_minmax_multi/brin_minmax_multi.control
 rename {src/test/regress => contrib/brin_minmax_multi}/expected/brin_multi.out (97%)
 rename {src/test/regress => contrib/brin_minmax_multi}/sql/brin_multi.sql (97%)

diff --git a/contrib/Makefile b/contrib/Makefile
index f878287429..bd131f8fc3 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -13,6 +13,7 @@ SUBDIRS = \
 		btree_gin	\
 		btree_gist	\
 		brin_bloom	\
+		brin_minmax_multi	\
 		citext		\
 		cube		\
 		dblink		\
diff --git a/contrib/brin_minmax_multi/.gitignore b/contrib/brin_minmax_multi/.gitignore
new file mode 100644
index 0000000000..5dcb3ff972
--- /dev/null
+++ b/contrib/brin_minmax_multi/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/brin_minmax_multi/Makefile b/contrib/brin_minmax_multi/Makefile
new file mode 100644
index 0000000000..9ca5112339
--- /dev/null
+++ b/contrib/brin_minmax_multi/Makefile
@@ -0,0 +1,23 @@
+# contrib/brin_minmax_multi/Makefile
+
+MODULE_big = brin_minmax_multi
+OBJS = \
+	$(WIN32RES) \
+	brin_minmax_multi.o
+
+EXTENSION = brin_minmax_multi
+DATA = brin_minmax_multi--1.0.sql
+PGFILEDESC = "brin_minmax_multi - BRIN minmax-multi operator classes"
+
+REGRESS = brin_multi
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/brin_minmax_multi
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/brin_minmax_multi/brin_minmax_multi--1.0.sql b/contrib/brin_minmax_multi/brin_minmax_multi--1.0.sql
new file mode 100644
index 0000000000..0e32d38bd9
--- /dev/null
+++ b/contrib/brin_minmax_multi/brin_minmax_multi--1.0.sql
@@ -0,0 +1,552 @@
+/* contrib/brin_minmax_multi/brin_minmax_multi--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION brin_minmax_multi" to load this file. \quit
+
+CREATE TYPE brin_minmax_multi_summary;
+
+-- BRIN bloom summary data type
+CREATE FUNCTION brin_minmax_multi_summary_in(cstring)
+RETURNS brin_minmax_multi_summary
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_minmax_multi_summary_out(brin_minmax_multi_summary)
+RETURNS cstring
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_minmax_multi_summary_recv(internal)
+RETURNS brin_minmax_multi_summary
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION brin_minmax_multi_summary_send(brin_minmax_multi_summary)
+RETURNS bytea
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE TYPE brin_minmax_multi_summary (
+	INTERNALLENGTH = -1,
+	INPUT = brin_minmax_multi_summary_in,
+	OUTPUT = brin_minmax_multi_summary_out,
+	RECEIVE = brin_minmax_multi_summary_recv,
+	SEND = brin_minmax_multi_summary_send,
+	STORAGE = extended
+);
+
+-- BRIN support procedures
+CREATE FUNCTION brin_minmax_multi_opcinfo(internal)
+RETURNS internal
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_add_value(internal, internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_consistent(internal, internal, internal, int4)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_union(internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_options(internal)
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+-- distance functions
+
+CREATE FUNCTION brin_minmax_multi_distance_int2(int2, int2)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_int4(int4, int4)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_int8(int8, int8)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_float4(float4, float4)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_float8(float8, float8)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_numeric(numeric, numeric)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_tid(tid, tid)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_uuid(uuid, uuid)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_date(date, date)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_time(time, time)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_interval(interval, interval)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_timetz(timetz, timetz)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_pg_lsn(pg_lsn, pg_lsn)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_macaddr(macaddr, macaddr)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_macaddr8(macaddr8, macaddr8)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_inet(inet, inet)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE FUNCTION brin_minmax_multi_distance_timestamp(timestamp, timestamp)
+RETURNS float8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE;
+
+CREATE OPERATOR FAMILY datetime_minmax_multi_ops USING brin;
+
+CREATE OPERATOR CLASS date_minmax_multi_ops
+FOR TYPE date USING brin FAMILY datetime_minmax_multi_ops
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_date(date,date),
+STORAGE         date;
+
+CREATE OPERATOR FAMILY float_minmax_multi_ops USING brin;
+
+CREATE OPERATOR CLASS float4_minmax_multi_ops
+FOR TYPE float4 USING brin FAMILY float_minmax_multi_ops
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_float4(float4,float4),
+STORAGE         float4;
+
+CREATE OPERATOR CLASS float8_minmax_multi_ops
+FOR TYPE float8 USING brin FAMILY float_minmax_multi_ops
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_float8(float8,float8),
+STORAGE         float8;
+
+ALTER OPERATOR FAMILY float_minmax_multi_ops USING brin ADD
+
+    OPERATOR        1       <(float4,float8),
+    OPERATOR        2       <=(float4,float8),
+    OPERATOR        3       =(float4,float8),
+    OPERATOR        4       >=(float4,float8),
+    OPERATOR        5       >(float4,float8),
+
+    OPERATOR        1       <(float8,float4),
+    OPERATOR        2       <=(float8,float4),
+    OPERATOR        3       =(float8,float4),
+    OPERATOR        4       >=(float8,float4),
+    OPERATOR        5       >(float8,float4);
+
+CREATE OPERATOR FAMILY network_minmax_multi_ops USING brin;
+
+CREATE OPERATOR CLASS inet_minmax_multi_ops
+FOR TYPE inet USING brin FAMILY network_minmax_multi_ops
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_inet(inet,inet),
+STORAGE         inet;
+
+CREATE OPERATOR FAMILY integer_minmax_multi_ops USING brin;
+
+CREATE OPERATOR CLASS int2_minmax_multi_ops
+FOR TYPE int2 USING brin FAMILY integer_minmax_multi_ops
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_int2(int2,int2),
+STORAGE         int2;
+
+CREATE OPERATOR CLASS int4_minmax_multi_ops
+FOR TYPE int4 USING brin FAMILY integer_minmax_multi_ops
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_int4(int4,int4),
+STORAGE         int4;
+
+CREATE OPERATOR CLASS int8_minmax_multi_ops
+FOR TYPE int8 USING brin FAMILY integer_minmax_multi_ops
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_int8(int8,int8),
+STORAGE         int8;
+
+ALTER OPERATOR FAMILY integer_minmax_multi_ops USING brin ADD
+
+    OPERATOR        1       <(int2,int4),
+    OPERATOR        2       <=(int2,int4),
+    OPERATOR        3       =(int2,int4),
+    OPERATOR        4       >=(int2,int4),
+    OPERATOR        5       >(int2,int4),
+
+    OPERATOR        1       <(int2,int8),
+    OPERATOR        2       <=(int2,int8),
+    OPERATOR        3       =(int2,int8),
+    OPERATOR        4       >=(int2,int8),
+    OPERATOR        5       >(int2,int8),
+
+    OPERATOR        1       <(int4,int2),
+    OPERATOR        2       <=(int4,int2),
+    OPERATOR        3       =(int4,int2),
+    OPERATOR        4       >=(int4,int2),
+    OPERATOR        5       >(int4,int2),
+
+    OPERATOR        1       <(int4,int8),
+    OPERATOR        2       <=(int4,int8),
+    OPERATOR        3       =(int4,int8),
+    OPERATOR        4       >=(int4,int8),
+    OPERATOR        5       >(int4,int8),
+
+    OPERATOR        1       <(int8,int2),
+    OPERATOR        2       <=(int8,int2),
+    OPERATOR        3       =(int8,int2),
+    OPERATOR        4       >=(int8,int2),
+    OPERATOR        5       >(int8,int2),
+
+    OPERATOR        1       <(int8,int4),
+    OPERATOR        2       <=(int8,int4),
+    OPERATOR        3       =(int8,int4),
+    OPERATOR        4       >=(int8,int4),
+    OPERATOR        5       >(int8,int4);
+
+CREATE OPERATOR CLASS interval_minmax_multi_ops
+FOR TYPE interval USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_interval(interval,interval),
+STORAGE         interval;
+
+CREATE OPERATOR CLASS macaddr_minmax_multi_ops
+FOR TYPE macaddr USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_macaddr(macaddr,macaddr),
+STORAGE         macaddr;
+
+CREATE OPERATOR CLASS macaddr8_minmax_multi_ops
+FOR TYPE macaddr8 USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_macaddr8(macaddr8,macaddr8),
+STORAGE         macaddr8;
+
+CREATE OPERATOR CLASS numeric_minmax_multi_ops
+FOR TYPE numeric USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_numeric(numeric,numeric),
+STORAGE         numeric;
+
+CREATE OPERATOR CLASS oid_minmax_multi_ops
+FOR TYPE oid USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_int4(int4,int4),
+STORAGE         oid;
+
+CREATE OPERATOR CLASS pg_lsn_minmax_multi_ops
+FOR TYPE pg_lsn USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_pg_lsn(pg_lsn,pg_lsn),
+STORAGE         pg_lsn;
+
+CREATE OPERATOR CLASS tid_minmax_multi_ops
+FOR TYPE tid USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_tid(tid,tid),
+STORAGE         tid;
+
+CREATE OPERATOR CLASS time_minmax_multi_ops
+FOR TYPE time USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_time(time,time),
+STORAGE         time;
+
+CREATE OPERATOR CLASS timestamp_minmax_multi_ops
+FOR TYPE timestamp USING brin FAMILY datetime_minmax_multi_ops
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_timestamp(timestamp,timestamp),
+STORAGE         timestamp;
+
+CREATE OPERATOR CLASS timestamptz_minmax_multi_ops
+FOR TYPE timestamptz USING brin FAMILY datetime_minmax_multi_ops
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_timestamp(timestamp,timestamp),
+STORAGE         timestamptz;
+
+ALTER OPERATOR FAMILY datetime_minmax_multi_ops USING brin ADD
+
+    OPERATOR        1       <(timestamp,timestamptz),
+    OPERATOR        2       <=(timestamp,timestamptz),
+    OPERATOR        3       =(timestamp,timestamptz),
+    OPERATOR        4       >=(timestamp,timestamptz),
+    OPERATOR        5       >(timestamp,timestamptz),
+
+    OPERATOR        1       <(timestamp,date),
+    OPERATOR        2       <=(timestamp,date),
+    OPERATOR        3       =(timestamp,date),
+    OPERATOR        4       >=(timestamp,date),
+    OPERATOR        5       >(timestamp,date),
+
+    OPERATOR        1       <(date,timestamp),
+    OPERATOR        2       <=(date,timestamp),
+    OPERATOR        3       =(date,timestamp),
+    OPERATOR        4       >=(date,timestamp),
+    OPERATOR        5       >(date,timestamp),
+
+    OPERATOR        1       <(date,timestamptz),
+    OPERATOR        2       <=(date,timestamptz),
+    OPERATOR        3       =(date,timestamptz),
+    OPERATOR        4       >=(date,timestamptz),
+    OPERATOR        5       >(date,timestamptz),
+
+    OPERATOR        1       <(timestamptz,timestamp),
+    OPERATOR        2       <=(timestamptz,timestamp),
+    OPERATOR        3       =(timestamptz,timestamp),
+    OPERATOR        4       >=(timestamptz,timestamp),
+    OPERATOR        5       >(timestamptz,timestamp),
+
+    OPERATOR        1       <(timestamptz,date),
+    OPERATOR        2       <=(timestamptz,date),
+    OPERATOR        3       =(timestamptz,date),
+    OPERATOR        4       >=(timestamptz,date),
+    OPERATOR        5       >(timestamptz,date);
+
+CREATE OPERATOR CLASS timetz_minmax_multi_ops
+FOR TYPE timetz USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_timetz(timetz,timetz),
+STORAGE         timetz;
+
+CREATE OPERATOR CLASS uuid_minmax_multi_ops
+FOR TYPE uuid USING brin
+AS
+    OPERATOR        1       <,
+    OPERATOR        2       <=,
+    OPERATOR        3       =,
+    OPERATOR        4       >=,
+    OPERATOR        5       >,
+    FUNCTION        1       brin_minmax_multi_opcinfo(internal),
+    FUNCTION        2       brin_minmax_multi_add_value(internal, internal, internal, internal),
+    FUNCTION        3       brin_minmax_multi_consistent(internal, internal, internal, int4),
+    FUNCTION        4       brin_minmax_multi_union(internal, internal, internal),
+    FUNCTION        5       brin_minmax_multi_options(internal),
+    FUNCTION       11       brin_minmax_multi_distance_uuid(uuid,uuid),
+STORAGE         uuid;
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/contrib/brin_minmax_multi/brin_minmax_multi.c
similarity index 96%
rename from src/backend/access/brin/brin_minmax_multi.c
rename to contrib/brin_minmax_multi/brin_minmax_multi.c
index 1ca86b7fb7..bb6a9b9157 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/contrib/brin_minmax_multi/brin_minmax_multi.c
@@ -66,6 +66,7 @@
 #include "access/reloptions.h"
 #include "access/stratnum.h"
 #include "access/htup_details.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_amop.h"
@@ -83,6 +84,8 @@
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
 
+PG_MODULE_MAGIC;
+
 /*
  * Additional SQL level support functions
  *
@@ -257,6 +260,63 @@ static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
 													uint16 attno, Oid subtype,
 													uint16 strategynum);
 
+Datum brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_date(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_time(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_add_value(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_consistent(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_union(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_options(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_summary_in(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_summary_out(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_summary_recv(PG_FUNCTION_ARGS);
+Datum brin_minmax_multi_summary_send(PG_FUNCTION_ARGS);
+
+PG_FUNCTION_INFO_V1(brin_minmax_multi_summary_in);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_summary_out);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_summary_send);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_summary_recv);
+
+PG_FUNCTION_INFO_V1(brin_minmax_multi_opcinfo);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_options);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_add_value);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_consistent);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_union);
+
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_int2);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_int4);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_int8);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_float4);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_float8);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_numeric);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_tid);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_uuid);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_date);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_time);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_interval);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_timetz);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_pg_lsn);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_macaddr);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_macaddr8);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_inet);
+PG_FUNCTION_INFO_V1(brin_minmax_multi_distance_timestamp);
+
+
 typedef struct compare_context
 {
 	FmgrInfo   *cmpFn;
@@ -1782,6 +1842,7 @@ Datum
 brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 {
 	BrinOpcInfo *result;
+	Oid	typoid = TypenameGetTypidExtended("brin_minmax_multi_summary", false);
 
 	/*
 	 * opaque->strategy_procinfos is initialized lazily; here it is set to
@@ -1794,7 +1855,7 @@ brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
 	result->oi_regular_nulls = true;
 	result->oi_opaque = (MinmaxMultiOpaque *)
 		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
-	result->oi_typcache[0] = lookup_type_cache(PG_BRIN_MINMAX_MULTI_SUMMARYOID, 0);
+	result->oi_typcache[0] = lookup_type_cache(typoid, 0);
 
 	PG_RETURN_POINTER(result);
 }
diff --git a/contrib/brin_minmax_multi/brin_minmax_multi.control b/contrib/brin_minmax_multi/brin_minmax_multi.control
new file mode 100644
index 0000000000..71e614867a
--- /dev/null
+++ b/contrib/brin_minmax_multi/brin_minmax_multi.control
@@ -0,0 +1,6 @@
+# brin_minmax_multi extension
+comment = 'support for BRIN minmax-multi indexes'
+default_version = '1.0'
+module_pathname = '$libdir/brin_minmax_multi'
+relocatable = true
+trusted = true
diff --git a/src/test/regress/expected/brin_multi.out b/contrib/brin_minmax_multi/expected/brin_multi.out
similarity index 97%
rename from src/test/regress/expected/brin_multi.out
rename to contrib/brin_minmax_multi/expected/brin_multi.out
index 0a7e4b8060..027d45c308 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/contrib/brin_minmax_multi/expected/brin_multi.out
@@ -1,3 +1,24 @@
+CREATE EXTENSION brin_minmax_multi;
+CREATE TABLE tenk1 (
+	unique1		int4,
+	unique2		int4,
+	two			int4,
+	four		int4,
+	ten			int4,
+	twenty		int4,
+	hundred		int4,
+	thousand	int4,
+	twothousand	int4,
+	fivethous	int4,
+	tenthous	int4,
+	odd			int4,
+	even		int4,
+	stringu1	name,
+	stringu2	name,
+	string4		name
+);
+\copy tenk1 from '../../src/test/regress/data/tenk.data'
+CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops);
 CREATE TABLE brintest_multi (
 	int8col bigint,
 	int2col smallint,
diff --git a/src/test/regress/sql/brin_multi.sql b/contrib/brin_minmax_multi/sql/brin_multi.sql
similarity index 97%
rename from src/test/regress/sql/brin_multi.sql
rename to contrib/brin_minmax_multi/sql/brin_multi.sql
index f5ffcaea33..c7b87c1ca3 100644
--- a/src/test/regress/sql/brin_multi.sql
+++ b/contrib/brin_minmax_multi/sql/brin_multi.sql
@@ -1,3 +1,28 @@
+CREATE EXTENSION brin_minmax_multi;
+
+CREATE TABLE tenk1 (
+	unique1		int4,
+	unique2		int4,
+	two			int4,
+	four		int4,
+	ten			int4,
+	twenty		int4,
+	hundred		int4,
+	thousand	int4,
+	twothousand	int4,
+	fivethous	int4,
+	tenthous	int4,
+	odd			int4,
+	even		int4,
+	stringu1	name,
+	stringu2	name,
+	string4		name
+);
+
+\copy tenk1 from '../../src/test/regress/data/tenk.data'
+
+CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops);
+
 CREATE TABLE brintest_multi (
 	int8col bigint,
 	int2col smallint,
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 75eb87ec10..468e1e289a 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -16,7 +16,6 @@ OBJS = \
 	brin.o \
 	brin_inclusion.o \
 	brin_minmax.o \
-	brin_minmax_multi.o \
 	brin_pageops.o \
 	brin_revmap.o \
 	brin_tuple.o \
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 51bef55a57..0f7ff63669 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1994,152 +1994,6 @@
   amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
   amopmethod => 'brin' },
 
-# minmax multi integer
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int8,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int8,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int8,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int8,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int8,int8)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int8,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int8,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int8,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int8,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int8,int2)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int8,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int8,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int8,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int8,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int8',
-  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int8,int4)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int2,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int2,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int2,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int2,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int2,int2)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int2,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int2,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int2,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int2,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int2,int8)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int2,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int2,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int2,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int2,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int2',
-  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int2,int4)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '1', amopopr => '<(int4,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '2', amopopr => '<=(int4,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '3', amopopr => '=(int4,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '4', amopopr => '>=(int4,int4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int4', amopstrategy => '5', amopopr => '>(int4,int4)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int2', amopstrategy => '1', amopopr => '<(int4,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int2', amopstrategy => '2', amopopr => '<=(int4,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int2', amopstrategy => '3', amopopr => '=(int4,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int2', amopstrategy => '4', amopopr => '>=(int4,int2)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int2', amopstrategy => '5', amopopr => '>(int4,int2)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int8', amopstrategy => '1', amopopr => '<(int4,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int8', amopstrategy => '2', amopopr => '<=(int4,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int8', amopstrategy => '3', amopopr => '=(int4,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int8', amopstrategy => '4', amopopr => '>=(int4,int8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/integer_minmax_multi_ops', amoplefttype => 'int4',
-  amoprighttype => 'int8', amopstrategy => '5', amopopr => '>(int4,int8)',
-  amopmethod => 'brin' },
-
 # minmax text
 { amopfamily => 'brin/text_minmax_ops', amoplefttype => 'text',
   amoprighttype => 'text', amopstrategy => '1', amopopr => '<(text,text)',
@@ -2174,23 +2028,6 @@
   amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
   amopmethod => 'brin' },
 
-# minmax multi oid
-{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '1', amopopr => '<(oid,oid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '2', amopopr => '<=(oid,oid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '3', amopopr => '=(oid,oid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '4', amopopr => '>=(oid,oid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/oid_minmax_multi_ops', amoplefttype => 'oid',
-  amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)',
-  amopmethod => 'brin' },
-
 # minmax tid
 { amopfamily => 'brin/tid_minmax_ops', amoplefttype => 'tid',
   amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
@@ -2208,23 +2045,6 @@
   amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
   amopmethod => 'brin' },
 
-# minmax multi tid
-{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '1', amopopr => '<(tid,tid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '2', amopopr => '<=(tid,tid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '3', amopopr => '=(tid,tid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '4', amopopr => '>=(tid,tid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/tid_minmax_multi_ops', amoplefttype => 'tid',
-  amoprighttype => 'tid', amopstrategy => '5', amopopr => '>(tid,tid)',
-  amopmethod => 'brin' },
-
 # minmax float (float4, float8)
 
 { amopfamily => 'brin/float_minmax_ops', amoplefttype => 'float4',
@@ -2291,72 +2111,6 @@
   amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
   amopmethod => 'brin' },
 
-# minmax multi float (float4, float8)
-
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float4,float4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '2',
-  amopopr => '<=(float4,float4)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float4,float4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '4',
-  amopopr => '>=(float4,float4)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float4,float4)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float4,float8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float8', amopstrategy => '2',
-  amopopr => '<=(float4,float8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float4,float8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float8', amopstrategy => '4',
-  amopopr => '>=(float4,float8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float4',
-  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float4,float8)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float4', amopstrategy => '1', amopopr => '<(float8,float4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float4', amopstrategy => '2',
-  amopopr => '<=(float8,float4)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float4', amopstrategy => '3', amopopr => '=(float8,float4)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float4', amopstrategy => '4',
-  amopopr => '>=(float8,float4)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float4', amopstrategy => '5', amopopr => '>(float8,float4)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '1', amopopr => '<(float8,float8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '2',
-  amopopr => '<=(float8,float8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '3', amopopr => '=(float8,float8)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '4',
-  amopopr => '>=(float8,float8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/float_minmax_multi_ops', amoplefttype => 'float8',
-  amoprighttype => 'float8', amopstrategy => '5', amopopr => '>(float8,float8)',
-  amopmethod => 'brin' },
-
 # minmax macaddr
 { amopfamily => 'brin/macaddr_minmax_ops', amoplefttype => 'macaddr',
   amoprighttype => 'macaddr', amopstrategy => '1',
@@ -2374,23 +2128,6 @@
   amoprighttype => 'macaddr', amopstrategy => '5',
   amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
 
-# minmax multi macaddr
-{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '1',
-  amopopr => '<(macaddr,macaddr)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '2',
-  amopopr => '<=(macaddr,macaddr)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '3',
-  amopopr => '=(macaddr,macaddr)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '4',
-  amopopr => '>=(macaddr,macaddr)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr_minmax_multi_ops', amoplefttype => 'macaddr',
-  amoprighttype => 'macaddr', amopstrategy => '5',
-  amopopr => '>(macaddr,macaddr)', amopmethod => 'brin' },
-
 # minmax macaddr8
 { amopfamily => 'brin/macaddr8_minmax_ops', amoplefttype => 'macaddr8',
   amoprighttype => 'macaddr8', amopstrategy => '1',
@@ -2408,23 +2145,6 @@
   amoprighttype => 'macaddr8', amopstrategy => '5',
   amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
 
-# minmax multi macaddr8
-{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '1',
-  amopopr => '<(macaddr8,macaddr8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '2',
-  amopopr => '<=(macaddr8,macaddr8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '3',
-  amopopr => '=(macaddr8,macaddr8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '4',
-  amopopr => '>=(macaddr8,macaddr8)', amopmethod => 'brin' },
-{ amopfamily => 'brin/macaddr8_minmax_multi_ops', amoplefttype => 'macaddr8',
-  amoprighttype => 'macaddr8', amopstrategy => '5',
-  amopopr => '>(macaddr8,macaddr8)', amopmethod => 'brin' },
-
 # minmax inet
 { amopfamily => 'brin/network_minmax_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
@@ -2442,23 +2162,6 @@
   amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
   amopmethod => 'brin' },
 
-# minmax multi inet
-{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '1', amopopr => '<(inet,inet)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '2', amopopr => '<=(inet,inet)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '3', amopopr => '=(inet,inet)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '4', amopopr => '>=(inet,inet)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/network_minmax_multi_ops', amoplefttype => 'inet',
-  amoprighttype => 'inet', amopstrategy => '5', amopopr => '>(inet,inet)',
-  amopmethod => 'brin' },
-
 # inclusion inet
 { amopfamily => 'brin/network_inclusion_ops', amoplefttype => 'inet',
   amoprighttype => 'inet', amopstrategy => '3', amopopr => '&&(inet,inet)',
@@ -2513,23 +2216,6 @@
   amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
   amopmethod => 'brin' },
 
-# minmax multi time without time zone
-{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '1', amopopr => '<(time,time)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '2', amopopr => '<=(time,time)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '3', amopopr => '=(time,time)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '4', amopopr => '>=(time,time)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/time_minmax_multi_ops', amoplefttype => 'time',
-  amoprighttype => 'time', amopstrategy => '5', amopopr => '>(time,time)',
-  amopmethod => 'brin' },
-
 # minmax datetime (date, timestamp, timestamptz)
 
 { amopfamily => 'brin/datetime_minmax_ops', amoplefttype => 'timestamp',
@@ -2676,152 +2362,6 @@
   amoprighttype => 'timestamptz', amopstrategy => '5',
   amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
 
-# minmax multi datetime (date, timestamp, timestamptz)
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '1',
-  amopopr => '<(timestamp,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '2',
-  amopopr => '<=(timestamp,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '3',
-  amopopr => '=(timestamp,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '4',
-  amopopr => '>=(timestamp,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamp', amopstrategy => '5',
-  amopopr => '>(timestamp,timestamp)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(timestamp,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(timestamp,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(timestamp,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(timestamp,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(timestamp,date)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamptz', amopstrategy => '1',
-  amopopr => '<(timestamp,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamptz', amopstrategy => '2',
-  amopopr => '<=(timestamp,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamptz', amopstrategy => '3',
-  amopopr => '=(timestamp,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamptz', amopstrategy => '4',
-  amopopr => '>=(timestamp,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamp',
-  amoprighttype => 'timestamptz', amopstrategy => '5',
-  amopopr => '>(timestamp,timestamptz)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '1', amopopr => '<(date,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '2', amopopr => '<=(date,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '3', amopopr => '=(date,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '4', amopopr => '>=(date,date)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'date', amopstrategy => '5', amopopr => '>(date,date)',
-  amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamp', amopstrategy => '1',
-  amopopr => '<(date,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamp', amopstrategy => '2',
-  amopopr => '<=(date,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamp', amopstrategy => '3',
-  amopopr => '=(date,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamp', amopstrategy => '4',
-  amopopr => '>=(date,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamp', amopstrategy => '5',
-  amopopr => '>(date,timestamp)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamptz', amopstrategy => '1',
-  amopopr => '<(date,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamptz', amopstrategy => '2',
-  amopopr => '<=(date,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamptz', amopstrategy => '3',
-  amopopr => '=(date,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamptz', amopstrategy => '4',
-  amopopr => '>=(date,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'date',
-  amoprighttype => 'timestamptz', amopstrategy => '5',
-  amopopr => '>(date,timestamptz)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'date', amopstrategy => '1',
-  amopopr => '<(timestamptz,date)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'date', amopstrategy => '2',
-  amopopr => '<=(timestamptz,date)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'date', amopstrategy => '3',
-  amopopr => '=(timestamptz,date)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'date', amopstrategy => '4',
-  amopopr => '>=(timestamptz,date)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'date', amopstrategy => '5',
-  amopopr => '>(timestamptz,date)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamp', amopstrategy => '1',
-  amopopr => '<(timestamptz,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamp', amopstrategy => '2',
-  amopopr => '<=(timestamptz,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamp', amopstrategy => '3',
-  amopopr => '=(timestamptz,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamp', amopstrategy => '4',
-  amopopr => '>=(timestamptz,timestamp)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamp', amopstrategy => '5',
-  amopopr => '>(timestamptz,timestamp)', amopmethod => 'brin' },
-
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '1',
-  amopopr => '<(timestamptz,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '2',
-  amopopr => '<=(timestamptz,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '3',
-  amopopr => '=(timestamptz,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '4',
-  amopopr => '>=(timestamptz,timestamptz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/datetime_minmax_multi_ops', amoplefttype => 'timestamptz',
-  amoprighttype => 'timestamptz', amopstrategy => '5',
-  amopopr => '>(timestamptz,timestamptz)', amopmethod => 'brin' },
-
 # minmax interval
 { amopfamily => 'brin/interval_minmax_ops', amoplefttype => 'interval',
   amoprighttype => 'interval', amopstrategy => '1',
@@ -2839,23 +2379,6 @@
   amoprighttype => 'interval', amopstrategy => '5',
   amopopr => '>(interval,interval)', amopmethod => 'brin' },
 
-# minmax multi interval
-{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '1',
-  amopopr => '<(interval,interval)', amopmethod => 'brin' },
-{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '2',
-  amopopr => '<=(interval,interval)', amopmethod => 'brin' },
-{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '3',
-  amopopr => '=(interval,interval)', amopmethod => 'brin' },
-{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '4',
-  amopopr => '>=(interval,interval)', amopmethod => 'brin' },
-{ amopfamily => 'brin/interval_minmax_multi_ops', amoplefttype => 'interval',
-  amoprighttype => 'interval', amopstrategy => '5',
-  amopopr => '>(interval,interval)', amopmethod => 'brin' },
-
 # minmax time with time zone
 { amopfamily => 'brin/timetz_minmax_ops', amoplefttype => 'timetz',
   amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
@@ -2873,23 +2396,6 @@
   amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
   amopmethod => 'brin' },
 
-# minmax multi time with time zone
-{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '1', amopopr => '<(timetz,timetz)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '2',
-  amopopr => '<=(timetz,timetz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '3', amopopr => '=(timetz,timetz)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '4',
-  amopopr => '>=(timetz,timetz)', amopmethod => 'brin' },
-{ amopfamily => 'brin/timetz_minmax_multi_ops', amoplefttype => 'timetz',
-  amoprighttype => 'timetz', amopstrategy => '5', amopopr => '>(timetz,timetz)',
-  amopmethod => 'brin' },
-
 # minmax bit
 { amopfamily => 'brin/bit_minmax_ops', amoplefttype => 'bit',
   amoprighttype => 'bit', amopstrategy => '1', amopopr => '<(bit,bit)',
@@ -2941,23 +2447,6 @@
   amoprighttype => 'numeric', amopstrategy => '5',
   amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
 
-# minmax multi numeric
-{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '1',
-  amopopr => '<(numeric,numeric)', amopmethod => 'brin' },
-{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '2',
-  amopopr => '<=(numeric,numeric)', amopmethod => 'brin' },
-{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '3',
-  amopopr => '=(numeric,numeric)', amopmethod => 'brin' },
-{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '4',
-  amopopr => '>=(numeric,numeric)', amopmethod => 'brin' },
-{ amopfamily => 'brin/numeric_minmax_multi_ops', amoplefttype => 'numeric',
-  amoprighttype => 'numeric', amopstrategy => '5',
-  amopopr => '>(numeric,numeric)', amopmethod => 'brin' },
-
 # minmax uuid
 { amopfamily => 'brin/uuid_minmax_ops', amoplefttype => 'uuid',
   amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
@@ -2975,23 +2464,6 @@
   amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
   amopmethod => 'brin' },
 
-# minmax multi uuid
-{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '1', amopopr => '<(uuid,uuid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '2', amopopr => '<=(uuid,uuid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '3', amopopr => '=(uuid,uuid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '4', amopopr => '>=(uuid,uuid)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/uuid_minmax_multi_ops', amoplefttype => 'uuid',
-  amoprighttype => 'uuid', amopstrategy => '5', amopopr => '>(uuid,uuid)',
-  amopmethod => 'brin' },
-
 # inclusion range types
 { amopfamily => 'brin/range_inclusion_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '1',
@@ -3053,23 +2525,6 @@
   amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
   amopmethod => 'brin' },
 
-# minmax multi pg_lsn
-{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '1', amopopr => '<(pg_lsn,pg_lsn)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '2',
-  amopopr => '<=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
-{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '3', amopopr => '=(pg_lsn,pg_lsn)',
-  amopmethod => 'brin' },
-{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '4',
-  amopopr => '>=(pg_lsn,pg_lsn)', amopmethod => 'brin' },
-{ amopfamily => 'brin/pg_lsn_minmax_multi_ops', amoplefttype => 'pg_lsn',
-  amoprighttype => 'pg_lsn', amopstrategy => '5', amopopr => '>(pg_lsn,pg_lsn)',
-  amopmethod => 'brin' },
-
 # inclusion box
 { amopfamily => 'brin/box_inclusion_ops', amoplefttype => 'box',
   amoprighttype => 'box', amopstrategy => '1', amopopr => '<<(box,box)',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 68c3342eeb..9192a66a88 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -932,152 +932,6 @@
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# minmax multi integer: int2, int4, int8
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int2' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int8' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
-
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1',
@@ -1103,23 +957,6 @@
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# minmax multi oid
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_int4' },
-
 # minmax tid
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1132,23 +969,6 @@
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# minmax multi tid
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '1', amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
-  amprocrighttype => 'tid', amprocnum => '11', amproc => 'brin_minmax_multi_distance_tid' },
-
 # minmax float
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '1',
@@ -1199,80 +1019,6 @@
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi float
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float4', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_float4' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_float8' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float8', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_float8' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_float8' },
-
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '1',
@@ -1287,26 +1033,6 @@
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi macaddr
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
-  amprocrighttype => 'macaddr', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_macaddr' },
-
 # minmax macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '1',
@@ -1321,26 +1047,6 @@
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi macaddr8
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
-  amprocrighttype => 'macaddr8', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_macaddr8' },
-
 # minmax inet
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1354,26 +1060,6 @@
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# minmax multi inet
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
-  amprocrighttype => 'inet', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_inet' },
-
 # inclusion inet
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
@@ -1421,25 +1107,6 @@
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
 
-# minmax multi time without time zone
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
-  amprocrighttype => 'time', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_time' },
-
 # minmax datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '1',
@@ -1547,170 +1214,6 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi datetime (date, timestamp, timestamptz)
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_time' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_time' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_time' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_time' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_timestamp' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_timestamp' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_date' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_date' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_date' },
-
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '1',
@@ -1725,26 +1228,6 @@
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi interval
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
-  amprocrighttype => 'interval', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_interval' },
-
 # minmax time with time zone
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '1',
@@ -1759,26 +1242,6 @@
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi time with time zone
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
-  amprocrighttype => 'timetz', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_timetz' },
-
 # minmax bit
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '1', amproc => 'brin_minmax_opcinfo' },
@@ -1819,26 +1282,6 @@
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi numeric
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
-  amprocrighttype => 'numeric', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_numeric' },
-
 # minmax uuid
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '1',
@@ -1853,26 +1296,6 @@
   amprocrighttype => 'uuid', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi uuid
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_uuid' },
-
 # inclusion range types
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '1',
@@ -1910,26 +1333,6 @@
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
 
-# minmax multi pg_lsn
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '1',
-  amproc => 'brin_minmax_multi_opcinfo' },
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '2',
-  amproc => 'brin_minmax_multi_add_value' },
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '3',
-  amproc => 'brin_minmax_multi_consistent' },
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '4',
-  amproc => 'brin_minmax_multi_union' },
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '5',
-  amproc => 'brin_minmax_multi_options' },
-{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
-  amprocrighttype => 'pg_lsn', amprocnum => '11',
-  amproc => 'brin_minmax_multi_distance_pg_lsn' },
-
 # inclusion box
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '1',
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index e510e28655..24b1433e1f 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -275,64 +275,34 @@
 { opcmethod => 'brin', opcname => 'int8_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int8',
   opckeytype => 'int8' },
-{ opcmethod => 'brin', opcname => 'int8_minmax_multi_ops',
-  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int8',
-  opckeytype => 'int8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int2_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int2',
   opckeytype => 'int2' },
-{ opcmethod => 'brin', opcname => 'int2_minmax_multi_ops',
-  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int2',
-  opckeytype => 'int2', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'int4_minmax_ops',
   opcfamily => 'brin/integer_minmax_ops', opcintype => 'int4',
   opckeytype => 'int4' },
-{ opcmethod => 'brin', opcname => 'int4_minmax_multi_ops',
-  opcfamily => 'brin/integer_minmax_multi_ops', opcintype => 'int4',
-  opckeytype => 'int4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'text_minmax_ops',
   opcfamily => 'brin/text_minmax_ops', opcintype => 'text',
   opckeytype => 'text' },
 { opcmethod => 'brin', opcname => 'oid_minmax_ops',
   opcfamily => 'brin/oid_minmax_ops', opcintype => 'oid', opckeytype => 'oid' },
-{ opcmethod => 'brin', opcname => 'oid_minmax_multi_ops',
-  opcfamily => 'brin/oid_minmax_multi_ops', opcintype => 'oid',
-  opckeytype => 'oid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'tid_minmax_ops',
   opcfamily => 'brin/tid_minmax_ops', opcintype => 'tid', opckeytype => 'tid' },
-{ opcmethod => 'brin', opcname => 'tid_minmax_multi_ops',
-  opcfamily => 'brin/tid_minmax_multi_ops', opcintype => 'tid',
-  opckeytype => 'tid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float4_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float4',
   opckeytype => 'float4' },
-{ opcmethod => 'brin', opcname => 'float4_minmax_multi_ops',
-  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float4',
-  opckeytype => 'float4', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'float8_minmax_ops',
   opcfamily => 'brin/float_minmax_ops', opcintype => 'float8',
   opckeytype => 'float8' },
-{ opcmethod => 'brin', opcname => 'float8_minmax_multi_ops',
-  opcfamily => 'brin/float_minmax_multi_ops', opcintype => 'float8',
-  opckeytype => 'float8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr_minmax_ops',
   opcfamily => 'brin/macaddr_minmax_ops', opcintype => 'macaddr',
   opckeytype => 'macaddr' },
-{ opcmethod => 'brin', opcname => 'macaddr_minmax_multi_ops',
-  opcfamily => 'brin/macaddr_minmax_multi_ops', opcintype => 'macaddr',
-  opckeytype => 'macaddr', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'macaddr8_minmax_ops',
   opcfamily => 'brin/macaddr8_minmax_ops', opcintype => 'macaddr8',
   opckeytype => 'macaddr8' },
-{ opcmethod => 'brin', opcname => 'macaddr8_minmax_multi_ops',
-  opcfamily => 'brin/macaddr8_minmax_multi_ops', opcintype => 'macaddr8',
-  opckeytype => 'macaddr8', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_minmax_ops',
   opcfamily => 'brin/network_minmax_ops', opcintype => 'inet',
   opcdefault => 'f', opckeytype => 'inet' },
-{ opcmethod => 'brin', opcname => 'inet_minmax_multi_ops',
-  opcfamily => 'brin/network_minmax_multi_ops', opcintype => 'inet',
-  opcdefault => 'f', opckeytype => 'inet', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'inet_inclusion_ops',
   opcfamily => 'brin/network_inclusion_ops', opcintype => 'inet',
   opckeytype => 'inet' },
@@ -342,39 +312,21 @@
 { opcmethod => 'brin', opcname => 'time_minmax_ops',
   opcfamily => 'brin/time_minmax_ops', opcintype => 'time',
   opckeytype => 'time' },
-{ opcmethod => 'brin', opcname => 'time_minmax_multi_ops',
-  opcfamily => 'brin/time_minmax_multi_ops', opcintype => 'time',
-  opckeytype => 'time', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'date_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'date',
   opckeytype => 'date' },
-{ opcmethod => 'brin', opcname => 'date_minmax_multi_ops',
-  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'date',
-  opckeytype => 'date', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamp_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamp',
   opckeytype => 'timestamp' },
-{ opcmethod => 'brin', opcname => 'timestamp_minmax_multi_ops',
-  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamp',
-  opckeytype => 'timestamp', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timestamptz_minmax_ops',
   opcfamily => 'brin/datetime_minmax_ops', opcintype => 'timestamptz',
   opckeytype => 'timestamptz' },
-{ opcmethod => 'brin', opcname => 'timestamptz_minmax_multi_ops',
-  opcfamily => 'brin/datetime_minmax_multi_ops', opcintype => 'timestamptz',
-  opckeytype => 'timestamptz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'interval_minmax_ops',
   opcfamily => 'brin/interval_minmax_ops', opcintype => 'interval',
   opckeytype => 'interval' },
-{ opcmethod => 'brin', opcname => 'interval_minmax_multi_ops',
-  opcfamily => 'brin/interval_minmax_multi_ops', opcintype => 'interval',
-  opckeytype => 'interval', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'timetz_minmax_ops',
   opcfamily => 'brin/timetz_minmax_ops', opcintype => 'timetz',
   opckeytype => 'timetz' },
-{ opcmethod => 'brin', opcname => 'timetz_minmax_multi_ops',
-  opcfamily => 'brin/timetz_minmax_multi_ops', opcintype => 'timetz',
-  opckeytype => 'timetz', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'bit_minmax_ops',
   opcfamily => 'brin/bit_minmax_ops', opcintype => 'bit', opckeytype => 'bit' },
 { opcmethod => 'brin', opcname => 'varbit_minmax_ops',
@@ -383,27 +335,18 @@
 { opcmethod => 'brin', opcname => 'numeric_minmax_ops',
   opcfamily => 'brin/numeric_minmax_ops', opcintype => 'numeric',
   opckeytype => 'numeric' },
-{ opcmethod => 'brin', opcname => 'numeric_minmax_multi_ops',
-  opcfamily => 'brin/numeric_minmax_multi_ops', opcintype => 'numeric',
-  opckeytype => 'numeric', opcdefault => 'f' },
 
 # no brin opclass for record, anyarray
 
 { opcmethod => 'brin', opcname => 'uuid_minmax_ops',
   opcfamily => 'brin/uuid_minmax_ops', opcintype => 'uuid',
   opckeytype => 'uuid' },
-{ opcmethod => 'brin', opcname => 'uuid_minmax_multi_ops',
-  opcfamily => 'brin/uuid_minmax_multi_ops', opcintype => 'uuid',
-  opckeytype => 'uuid', opcdefault => 'f' },
 { opcmethod => 'brin', opcname => 'range_inclusion_ops',
   opcfamily => 'brin/range_inclusion_ops', opcintype => 'anyrange',
   opckeytype => 'anyrange' },
 { opcmethod => 'brin', opcname => 'pg_lsn_minmax_ops',
   opcfamily => 'brin/pg_lsn_minmax_ops', opcintype => 'pg_lsn',
   opckeytype => 'pg_lsn' },
-{ opcmethod => 'brin', opcname => 'pg_lsn_minmax_multi_ops',
-  opcfamily => 'brin/pg_lsn_minmax_multi_ops', opcintype => 'pg_lsn',
-  opckeytype => 'pg_lsn', opcdefault => 'f' },
 
 # no brin opclass for enum, tsvector, tsquery, jsonb
 
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 6218eade08..57e5aa0d8b 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -182,22 +182,14 @@
   opfmethod => 'gin', opfname => 'jsonb_path_ops' },
 { oid => '4054',
   opfmethod => 'brin', opfname => 'integer_minmax_ops' },
-{ oid => '4602',
-  opfmethod => 'brin', opfname => 'integer_minmax_multi_ops' },
 { oid => '4055',
   opfmethod => 'brin', opfname => 'numeric_minmax_ops' },
-{ oid => '4603',
-  opfmethod => 'brin', opfname => 'numeric_minmax_multi_ops' },
 { oid => '4056',
   opfmethod => 'brin', opfname => 'text_minmax_ops' },
 { oid => '4058',
   opfmethod => 'brin', opfname => 'timetz_minmax_ops' },
-{ oid => '4604',
-  opfmethod => 'brin', opfname => 'timetz_minmax_multi_ops' },
 { oid => '4059',
   opfmethod => 'brin', opfname => 'datetime_minmax_ops' },
-{ oid => '4605',
-  opfmethod => 'brin', opfname => 'datetime_minmax_multi_ops' },
 { oid => '4062',
   opfmethod => 'brin', opfname => 'char_minmax_ops' },
 { oid => '4064',
@@ -206,54 +198,34 @@
   opfmethod => 'brin', opfname => 'name_minmax_ops' },
 { oid => '4068',
   opfmethod => 'brin', opfname => 'oid_minmax_ops' },
-{ oid => '4606',
-  opfmethod => 'brin', opfname => 'oid_minmax_multi_ops' },
 { oid => '4069',
   opfmethod => 'brin', opfname => 'tid_minmax_ops' },
-{ oid => '4607',
-  opfmethod => 'brin', opfname => 'tid_minmax_multi_ops' },
 { oid => '4070',
   opfmethod => 'brin', opfname => 'float_minmax_ops' },
-{ oid => '4608',
-  opfmethod => 'brin', opfname => 'float_minmax_multi_ops' },
 { oid => '4074',
   opfmethod => 'brin', opfname => 'macaddr_minmax_ops' },
-{ oid => '4609',
-  opfmethod => 'brin', opfname => 'macaddr_minmax_multi_ops' },
 { oid => '4109',
   opfmethod => 'brin', opfname => 'macaddr8_minmax_ops' },
-{ oid => '4610',
-  opfmethod => 'brin', opfname => 'macaddr8_minmax_multi_ops' },
 { oid => '4075',
   opfmethod => 'brin', opfname => 'network_minmax_ops' },
-{ oid => '4611',
-  opfmethod => 'brin', opfname => 'network_minmax_multi_ops' },
 { oid => '4102',
   opfmethod => 'brin', opfname => 'network_inclusion_ops' },
 { oid => '4076',
   opfmethod => 'brin', opfname => 'bpchar_minmax_ops' },
 { oid => '4077',
   opfmethod => 'brin', opfname => 'time_minmax_ops' },
-{ oid => '4612',
-  opfmethod => 'brin', opfname => 'time_minmax_multi_ops' },
 { oid => '4078',
   opfmethod => 'brin', opfname => 'interval_minmax_ops' },
-{ oid => '4613',
-  opfmethod => 'brin', opfname => 'interval_minmax_multi_ops' },
 { oid => '4079',
   opfmethod => 'brin', opfname => 'bit_minmax_ops' },
 { oid => '4080',
   opfmethod => 'brin', opfname => 'varbit_minmax_ops' },
 { oid => '4081',
   opfmethod => 'brin', opfname => 'uuid_minmax_ops' },
-{ oid => '4614',
-  opfmethod => 'brin', opfname => 'uuid_minmax_multi_ops' },
 { oid => '4103',
   opfmethod => 'brin', opfname => 'range_inclusion_ops' },
 { oid => '4082',
   opfmethod => 'brin', opfname => 'pg_lsn_minmax_ops' },
-{ oid => '4615',
-  opfmethod => 'brin', opfname => 'pg_lsn_minmax_multi_ops' },
 { oid => '4104',
   opfmethod => 'brin', opfname => 'box_inclusion_ops' },
 { oid => '5000',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cb7d421d5a..ca240cef1a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8221,76 +8221,7 @@
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
 
-# BRIN minmax multi
-{ oid => '4616', descr => 'BRIN multi minmax support',
-  proname => 'brin_minmax_multi_opcinfo', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'brin_minmax_multi_opcinfo' },
-{ oid => '4617', descr => 'BRIN multi minmax support',
-  proname => 'brin_minmax_multi_add_value', prorettype => 'bool',
-  proargtypes => 'internal internal internal internal',
-  prosrc => 'brin_minmax_multi_add_value' },
-{ oid => '4618', descr => 'BRIN multi minmax support',
-  proname => 'brin_minmax_multi_consistent', prorettype => 'bool',
-  proargtypes => 'internal internal internal int4',
-  prosrc => 'brin_minmax_multi_consistent' },
-{ oid => '4619', descr => 'BRIN multi minmax support',
-  proname => 'brin_minmax_multi_union', prorettype => 'bool',
-  proargtypes => 'internal internal internal', prosrc => 'brin_minmax_multi_union' },
-{ oid => '4620', descr => 'BRIN multi minmax support',
-  proname => 'brin_minmax_multi_options', prorettype => 'void', proisstrict => 'f',
-  proargtypes => 'internal', prosrc => 'brin_minmax_multi_options' },
-
-{ oid => '4621', descr => 'BRIN multi minmax int2 distance',
-  proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int2' },
-{ oid => '4622', descr => 'BRIN multi minmax int4 distance',
-  proname => 'brin_minmax_multi_distance_int4', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int4' },
-{ oid => '4623', descr => 'BRIN multi minmax int8 distance',
-  proname => 'brin_minmax_multi_distance_int8', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_int8' },
-{ oid => '4624', descr => 'BRIN multi minmax float4 distance',
-  proname => 'brin_minmax_multi_distance_float4', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float4' },
-{ oid => '4625', descr => 'BRIN multi minmax float8 distance',
-  proname => 'brin_minmax_multi_distance_float8', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_float8' },
-{ oid => '4626', descr => 'BRIN multi minmax numeric distance',
-  proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_numeric' },
-{ oid => '4627', descr => 'BRIN multi minmax tid distance',
-  proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_tid' },
-{ oid => '4628', descr => 'BRIN multi minmax uuid distance',
-  proname => 'brin_minmax_multi_distance_uuid', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_uuid' },
-{ oid => '4629', descr => 'BRIN multi minmax date distance',
-  proname => 'brin_minmax_multi_distance_date', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_date' },
-{ oid => '4630', descr => 'BRIN multi minmax time distance',
-  proname => 'brin_minmax_multi_distance_time', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_time' },
-{ oid => '4631', descr => 'BRIN multi minmax interval distance',
-  proname => 'brin_minmax_multi_distance_interval', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_interval' },
-{ oid => '4632', descr => 'BRIN multi minmax timetz distance',
-  proname => 'brin_minmax_multi_distance_timetz', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timetz' },
-{ oid => '4633', descr => 'BRIN multi minmax pg_lsn distance',
-  proname => 'brin_minmax_multi_distance_pg_lsn', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_pg_lsn' },
-{ oid => '4634', descr => 'BRIN multi minmax macaddr distance',
-  proname => 'brin_minmax_multi_distance_macaddr', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr' },
-{ oid => '4635', descr => 'BRIN multi minmax macaddr8 distance',
-  proname => 'brin_minmax_multi_distance_macaddr8', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_macaddr8' },
-{ oid => '4636', descr => 'BRIN multi minmax inet distance',
-  proname => 'brin_minmax_multi_distance_inet', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_inet' },
-{ oid => '4637', descr => 'BRIN multi minmax timestamp distance',
-  proname => 'brin_minmax_multi_distance_timestamp', prorettype => 'float8',
-  proargtypes => 'internal internal', prosrc => 'brin_minmax_multi_distance_timestamp' },
+
 
 # BRIN inclusion
 { oid => '4105', descr => 'BRIN inclusion support',
@@ -11482,18 +11413,4 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
-{ oid => '4638', descr => 'I/O',
-  proname => 'brin_minmax_multi_summary_in', prorettype => 'pg_brin_minmax_multi_summary',
-  proargtypes => 'cstring', prosrc => 'brin_minmax_multi_summary_in' },
-{ oid => '4639', descr => 'I/O',
-  proname => 'brin_minmax_multi_summary_out', prorettype => 'cstring',
-  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_out' },
-{ oid => '4640', descr => 'I/O',
-  proname => 'brin_minmax_multi_summary_recv', provolatile => 's',
-  prorettype => 'pg_brin_minmax_multi_summary', proargtypes => 'internal',
-  prosrc => 'brin_minmax_multi_summary_recv' },
-{ oid => '4641', descr => 'I/O',
-  proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea',
-  proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' },
-
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 387d25d66a..920dbbbfeb 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -679,10 +679,4 @@
   typtype => 'p', typcategory => 'P', typinput => 'anycompatiblemultirange_in',
   typoutput => 'anycompatiblemultirange_out', typreceive => '-', typsend => '-',
   typalign => 'd', typstorage => 'x' },
-{ oid => '4601',
-  descr => 'BRIN minmax-multi summary',
-  typname => 'pg_brin_minmax_multi_summary', typlen => '-1', typbyval => 'f', typcategory => 'S',
-  typinput => 'brin_minmax_multi_summary_in', typoutput => 'brin_minmax_multi_summary_out',
-  typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send',
-  typalign => 'i', typstorage => 'x', typcollation => 'default' },
 ]
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index ee45e37b84..7204fdb0b4 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4990,12 +4990,11 @@ List of access methods
 (0 rows)
 
 \dAc brin pg*.oid*
-                      List of operator classes
-  AM  | Input type | Storage type |    Operator class    | Default? 
-------+------------+--------------+----------------------+----------
- brin | oid        |              | oid_minmax_multi_ops | no
- brin | oid        |              | oid_minmax_ops       | yes
-(2 rows)
+                   List of operator classes
+  AM  | Input type | Storage type | Operator class | Default? 
+------+------------+--------------+----------------+----------
+ brin | oid        |              | oid_minmax_ops | yes
+(1 row)
 
 \dAf spgist
           List of operator families
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 26f4b6097b..0c74dc96a8 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -67,14 +67,13 @@ WHERE p1.typtype not in ('p') AND p1.typname NOT LIKE E'\\_%'
      WHERE p2.typname = ('_' || p1.typname)::name AND
            p2.typelem = p1.oid and p1.typarray = p2.oid)
 ORDER BY p1.oid;
- oid  |           typname            
-------+------------------------------
+ oid  |     typname     
+------+-----------------
   194 | pg_node_tree
  3361 | pg_ndistinct
  3402 | pg_dependencies
- 4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(5 rows)
+(4 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1c6bbfcf54..70c38309d7 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -77,11 +77,6 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
-# ----------
-# Additional BRIN tests
-# ----------
-test: brin_multi
-
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index dd3f3a9f0b..d81d04136c 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -108,7 +108,6 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
-test: brin_multi
 test: gin
 test: gist
 test: spgist
-- 
2.30.2

#167Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Tomas Vondra (#165)
Re: WIP: BRIN multi-range indexes

On 2021-Mar-22, Tomas Vondra wrote:

I don't know what's the right fix, but it seems like this patch has
nothing to do with it. If we want to move the opclasses into an
extension, we can comment out that one (cidr/inet) case for now.

I don't know what would be a good reason to define the opclasses in
separate contrib extensions. I think it's going to be a nuisance to
users, so unless there is some strong argument for it, I'd suggest not
to do it. I found it being discussed here:
/messages/by-id/CA+TgmoajaQKBUx=vaTUFo6z80dsRzBw__Nu41Q4t06baZep3Ug@mail.gmail.com
but there weren't any strong arguments put forward.

It seems a good experiment to have done it, though, since we now know
that there is a limitation in the existing SQL interface. Maybe the fix
to that problem is to add a new clause to CREATE/ALTER OPERATOR CLASS to
let you define what goes into opckeytype. However I don't think it's
this patch's responsibility to fix that problem.

--
�lvaro Herrera Valdivia, Chile
"Hay que recordar que la existencia en el cosmos, y particularmente la
elaboraci�n de civilizaciones dentro de �l no son, por desgracia,
nada id�licas" (Ijon Tichy)

#168Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Alvaro Herrera (#167)
1 attachment(s)
Re: WIP: BRIN multi-range indexes

On 3/23/21 2:36 PM, Alvaro Herrera wrote:

On 2021-Mar-22, Tomas Vondra wrote:

I don't know what's the right fix, but it seems like this patch has
nothing to do with it. If we want to move the opclasses into an
extension, we can comment out that one (cidr/inet) case for now.

I don't know what would be a good reason to define the opclasses in
separate contrib extensions. I think it's going to be a nuisance to
users, so unless there is some strong argument for it, I'd suggest not
to do it. I found it being discussed here:
/messages/by-id/CA+TgmoajaQKBUx=vaTUFo6z80dsRzBw__Nu41Q4t06baZep3Ug@mail.gmail.com
but there weren't any strong arguments put forward.

OK, thanks for the opinion. Yeah, you're right there were no strong
opinions either way, and I see both pros/cons for the contrib option.
Defining the opclasses using SQL is way more convenient and less
error-prone than doing that directly in .dat files, for example. But
there are some limitations too, so not sure it's worth it.

It seems a good experiment to have done it, though, since we now know
that there is a limitation in the existing SQL interface. Maybe the fix
to that problem is to add a new clause to CREATE/ALTER OPERATOR CLASS to
let you define what goes into opckeytype. However I don't think it's
this patch's responsibility to fix that problem.

Yeah, it was a good experiment. I think we still need to figure out what
to do about the opckeytype - what needs to be fixed, exactly.

FWIW there's yet another difference between the current BRIN opclass
definition, compared to what CREATE OPERATOR CLASS would do. Or more
precisely, how we'd define opfamily for the cross-type cases (integer,
float and timestamp cases).

AFAICS we don't really need pg_amproc entries for the cross-type cases,
we just need the operators, so pg_amproc entries like

{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype =>
'timestamptz',
amprocrighttype => 'timestamp', amprocnum => '1',
amproc => 'brin_minmax_opcinfo' },

are unnecessary. The attached patch cleans that up, without breaking any
regression tests. Or is there a reason why we need those?

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

brin_pg_amproc_cleanup.patchxtext/plain; charset=UTF-8; name=brin_pg_amproc_cleanup.patchxDownload
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 9192a66a88..50082a1801 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -843,28 +843,7 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_union' },
+
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '1',
   amproc => 'brin_minmax_opcinfo' },
@@ -876,28 +855,7 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_union' },
+
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '1',
   amproc => 'brin_minmax_opcinfo' },
@@ -909,28 +867,6 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
 
 # minmax text
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
@@ -982,18 +918,7 @@
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
-  amprocrighttype => 'float8', amprocnum => '4',
-  amproc => 'brin_minmax_union' },
+
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '1',
   amproc => 'brin_minmax_opcinfo' },
@@ -1006,18 +931,6 @@
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
-  amprocrighttype => 'float4', amprocnum => '4',
-  amproc => 'brin_minmax_union' },
 
 # minmax macaddr
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
@@ -1120,29 +1033,7 @@
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4',
   amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamptz', amprocnum => '4',
-  amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_minmax_union' },
+
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '1',
   amproc => 'brin_minmax_opcinfo' },
@@ -1155,29 +1046,7 @@
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamp', amprocnum => '4',
-  amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_minmax_union' },
+
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'brin_minmax_opcinfo' },
@@ -1189,30 +1058,6 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamp', amprocnum => '4',
-  amproc => 'brin_minmax_union' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '1',
-  amproc => 'brin_minmax_opcinfo' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '2',
-  amproc => 'brin_minmax_add_value' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '3',
-  amproc => 'brin_minmax_consistent' },
-{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
-  amprocrighttype => 'timestamptz', amprocnum => '4',
-  amproc => 'brin_minmax_union' },
 
 # minmax interval
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
#169Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Tomas Vondra (#168)
Re: WIP: BRIN multi-range indexes

On 2021-Mar-23, Tomas Vondra wrote:

FWIW there's yet another difference between the current BRIN opclass
definition, compared to what CREATE OPERATOR CLASS would do. Or more
precisely, how we'd define opfamily for the cross-type cases (integer,
float and timestamp cases).

AFAICS we don't really need pg_amproc entries for the cross-type cases,
we just need the operators, so pg_amproc entries like

{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype =>
'timestamptz',
amprocrighttype => 'timestamp', amprocnum => '1',
amproc => 'brin_minmax_opcinfo' },

are unnecessary. The attached patch cleans that up, without breaking any
regression tests. Or is there a reason why we need those?

... ooh ...

When you say "just the operators" you mean the pg_amop entries, right?

I think I agree -- cross-type amproc entries are unlikely to have any
use.

--
�lvaro Herrera Valdivia, Chile

#170Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Alvaro Herrera (#169)
Re: WIP: BRIN multi-range indexes

On 3/23/21 7:28 PM, Alvaro Herrera wrote:

On 2021-Mar-23, Tomas Vondra wrote:

FWIW there's yet another difference between the current BRIN opclass
definition, compared to what CREATE OPERATOR CLASS would do. Or more
precisely, how we'd define opfamily for the cross-type cases (integer,
float and timestamp cases).

AFAICS we don't really need pg_amproc entries for the cross-type cases,
we just need the operators, so pg_amproc entries like

{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype =>
'timestamptz',
amprocrighttype => 'timestamp', amprocnum => '1',
amproc => 'brin_minmax_opcinfo' },

are unnecessary. The attached patch cleans that up, without breaking any
regression tests. Or is there a reason why we need those?

... ooh ...

When you say "just the operators" you mean the pg_amop entries, right?

I think I agree -- cross-type amproc entries are unlikely to have any
use.

I've pushed a cleanup of the unnecessary pg_amproc entries for the
built-in opclasses, and I have omitted them from the definition of the
new opclasses. Buildfarm seems happy, so far.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#171Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#166)
Re: WIP: BRIN multi-range indexes

Hi,

I've pushed both the bloom and minmax-multi indexes today.

Based on the feedback and limitations described before I decided to keep
them in core (i.e. not move them to contrib), but it was very useful
experiment as it uncovered a couple issues - both in existing code, and
in the definition of the new opclasses.

After further thought I've concluded that the decision to ditch the old
signature (in a1c649d889) was probably wrong, so I've added the support
back, per what Alexander originally proposed.

While doing so, I've noticed that the NULL handling may be a few bricks
shy, because with (oi_regular_nulls == false) it should probably keep
passing the NULL scan keys to the consistent function. I'll look into
that and get it fixed.

Thanks everyone who helped with those patches!

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#172Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Tomas Vondra (#171)
Re: WIP: BRIN multi-range indexes

On 2021-Mar-26, Tomas Vondra wrote:

I've pushed both the bloom and minmax-multi indexes today.

Congratulations! I think this reimplementation of the minmax opclass
infrastructure makes BRIN much more useful (read: actually usable).

--
�lvaro Herrera Valdivia, Chile

#173Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#171)
Re: WIP: BRIN multi-range indexes

On 3/26/21 2:08 PM, Tomas Vondra wrote:

Hi,

I've pushed both the bloom and minmax-multi indexes today.

Hmmm, I see a couple buildfarm animals are upset about the minmax-multi
patch. It does pass for me both on x86_64 and 32-bit ARM (rpi4), so I'm
unable to reproduce this. But most of the machines having issues seem to
be sparc variants, and snapper does this:

[New LWP 1471]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/sparc-linux-gnu/libthread_db.so.1".
Core was generated by `postgres: parallel worker for PID 1350
'.
Program terminated with signal 10, Bus error.
#0 0x007167fc in int82gt (fcinfo=0xffcc66d0) at int8.c:399
399 int64 val1 = PG_GETARG_INT64(0);
#0 0x007167fc in int82gt (fcinfo=0xffcc66d0) at int8.c:399
#1 0x00887a94 in FunctionCall2Coll (flinfo=0xb81a2c, collation=0,
arg1=12242916, arg2=0) at fmgr.c:1163

I recall seeing "bus error" on sparc with other patches because of
alignment issues, so I wonder if this is what's happening here.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#174Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tomas Vondra (#173)
Re: WIP: BRIN multi-range indexes

Tomas Vondra <tomas.vondra@enterprisedb.com> writes:

I recall seeing "bus error" on sparc with other patches because of
alignment issues, so I wonder if this is what's happening here.

Try compiling with the address sanitizer enabled. Per c.h,

* Testing can be done with "-fsanitize=alignment -fsanitize-trap=alignment"
* on clang, or "-fsanitize=alignment -fno-sanitize-recover=alignment" on gcc.

regards, tom lane

#175Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tom Lane (#174)
Re: WIP: BRIN multi-range indexes

On 3/26/21 2:55 PM, Tom Lane wrote:

Tomas Vondra <tomas.vondra@enterprisedb.com> writes:

I recall seeing "bus error" on sparc with other patches because of
alignment issues, so I wonder if this is what's happening here.

Try compiling with the address sanitizer enabled. Per c.h,

* Testing can be done with "-fsanitize=alignment -fsanitize-trap=alignment"
* on clang, or "-fsanitize=alignment -fno-sanitize-recover=alignment" on gcc.

Bingo! I see the one failing x86_64 machine has -fsanitize=alignment.

thanks

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#176Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#175)
Re: WIP: BRIN multi-range indexes

On 3/26/21 3:04 PM, Tomas Vondra wrote:

On 3/26/21 2:55 PM, Tom Lane wrote:

Tomas Vondra <tomas.vondra@enterprisedb.com> writes:

I recall seeing "bus error" on sparc with other patches because of
alignment issues, so I wonder if this is what's happening here.

Try compiling with the address sanitizer enabled. Per c.h,

* Testing can be done with "-fsanitize=alignment -fsanitize-trap=alignment"
* on clang, or "-fsanitize=alignment -fno-sanitize-recover=alignment" on gcc.

Bingo! I see the one failing x86_64 machine has -fsanitize=alignment.

Yeah, the deserialization is borked. It assumes it can just point into
the serialized representation of the summary, but that is "compacted" by
ignoring alignment. Hence the failures. For me it fails only for timetz
and interval types, but perhaps sparc is more sensitive, or maybe it's
about 32/64 bits too (the only backtrace I'm aware of is from snapper,
so assuming it's 32bits it'd make sense it fails on int8).

I have a feeling I made the same mistake in serialization of MCV stats
some time ago, shame on me. I'll get this fixed.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#177Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tomas Vondra (#176)
Re: WIP: BRIN multi-range indexes

On 3/26/21 3:45 PM, Tomas Vondra wrote:

On 3/26/21 3:04 PM, Tomas Vondra wrote:

On 3/26/21 2:55 PM, Tom Lane wrote:

Tomas Vondra <tomas.vondra@enterprisedb.com> writes:

I recall seeing "bus error" on sparc with other patches because of
alignment issues, so I wonder if this is what's happening here.

Try compiling with the address sanitizer enabled. Per c.h,

* Testing can be done with "-fsanitize=alignment -fsanitize-trap=alignment"
* on clang, or "-fsanitize=alignment -fno-sanitize-recover=alignment" on gcc.

Bingo! I see the one failing x86_64 machine has -fsanitize=alignment.

Yeah, the deserialization is borked. It assumes it can just point into
the serialized representation of the summary, but that is "compacted" by
ignoring alignment. Hence the failures. For me it fails only for timetz
and interval types, but perhaps sparc is more sensitive, or maybe it's
about 32/64 bits too (the only backtrace I'm aware of is from snapper,
so assuming it's 32bits it'd make sense it fails on int8).

I have a feeling I made the same mistake in serialization of MCV stats
some time ago, shame on me. I'll get this fixed.

I've pushed a fix, ensuring proper alignment. There was a second issue
that'd affect big endian systems, so I fixed that too.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#178Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Tomas Vondra (#171)
Re: WIP: BRIN multi-range indexes

On 2021-Mar-26, Tomas Vondra wrote:

Hi,

I've pushed both the bloom and minmax-multi indexes today.

One thing I've been wondering all along is how useful are these
BRIN-backed bloom indexes compared to contrib-supplied bloom indexes.
My guess is that the BRIN implementation has some advantage, or you
would not have worked so much on it. But what is it?

Thanks

--
�lvaro Herrera Valdivia, Chile

#179Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Alvaro Herrera (#178)
Re: WIP: BRIN multi-range indexes

On 3/27/21 7:09 PM, Alvaro Herrera wrote:

On 2021-Mar-26, Tomas Vondra wrote:

Hi,

I've pushed both the bloom and minmax-multi indexes today.

One thing I've been wondering all along is how useful are these
BRIN-backed bloom indexes compared to contrib-supplied bloom indexes.
My guess is that the BRIN implementation has some advantage, or you
would not have worked so much on it. But what is it?

The contrib/bloom indexes are a completely different type of index. They
are not BRIN but a completely separate AM. The bloom filters are per-row
(so the index is larger than BRIN) and it's useful when you have table
with many attributes, and need to test various combinations of them.

create table t (a int, b int, c int);

insert into t select 10 * random(), 10 * random(), 10 * random()
from generate_series(1,1000000) s(i);

analyze t;

create index bloom_idx on t using bloom (a,b,c)
with (length=80, col1=4, col2=4, col3=4);

create index brin_bloom_idx on t using
brin (a int4_bloom_ops, b int4_bloom_ops, c int4_bloom_ops);

test=# \di+
List of relations
Schema | Name | Table | Access Method | Size | Description
--------+----------------+-------+---------------+-------+-------------
public | bloom_idx | t | bloom | 15 MB |
public | brin_bloom_idx | t | brin | 88 kB |
(2 rows)

So it's a completely different kind of animal, perhaps closer to btree
than to BRIN. I'm sure there are cases where contrib/bloom works better
than brin/bloom, but also the other way around.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company